Merge remote-tracking branch 'origin/master'
29 files modified
1 files renamed
44 files added
86 files deleted
| | |
| | | "@vitejs/plugin-vue": "catalog:", |
| | | "@vue/compiler-sfc": "catalog:", |
| | | "postcss-pxtorem": "catalog:", |
| | | "prettier": "catalog:", |
| | | "sass": "catalog:", |
| | | "terser": "catalog:", |
| | | "unplugin-auto-import": "catalog:", |
| New file |
| | |
| | | <svg width="30" height="42" viewBox="0 0 30 42" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| | | <g id="Group 2136637024"> |
| | | <g id="Group 2136636871"> |
| | | <g id="Ellipse 115"> |
| | | <g filter="url(#filter0_i_201_2895)"> |
| | | <circle cx="15.0204" cy="14.9795" r="14.9795" fill="#082389"/> |
| | | <circle cx="15.0204" cy="14.9795" r="14.9795" fill="url(#paint0_linear_201_2895)" style="mix-blend-mode:overlay"/> |
| | | <circle cx="15.0204" cy="14.9795" r="14.9795" fill="url(#paint1_linear_201_2895)" style="mix-blend-mode:overlay"/> |
| | | </g> |
| | | <circle cx="15.0204" cy="14.9795" r="14.4795" stroke="white" style="mix-blend-mode:overlay"/> |
| | | </g> |
| | | <g id="Frame"> |
| | | <path id="Vector" d="M22.3395 20.5666C22.3395 21.2157 21.8137 21.7416 21.1644 21.7416H9.414C8.76486 21.7416 8.23898 21.2157 8.23898 20.5666V17.3352C8.23898 16.686 8.76488 16.1601 9.41401 16.1601H14.611V13.0757C14.611 12.6703 14.94 12.3412 15.3453 12.3412C15.7507 12.3412 16.0797 12.6703 16.0797 13.0757V16.1601H21.1644C21.8135 16.1601 22.3394 16.686 22.3394 17.3353L22.3395 20.5666ZM20.8707 18.9509C20.8707 18.5455 20.5417 18.2165 20.1362 18.2165C19.7309 18.2165 19.4019 18.5455 19.4019 18.9509C19.4019 19.3562 19.7309 19.6853 20.1362 19.6853C20.5417 19.6853 20.8707 19.3562 20.8707 18.9509ZM18.2268 18.9509C18.2268 18.5455 17.8978 18.2165 17.4925 18.2165C17.0871 18.2165 16.758 18.5455 16.758 18.9509C16.758 19.3562 17.0871 19.6853 17.4924 19.6853C17.8978 19.6853 18.2268 19.3562 18.2268 18.9509ZM15.3454 9.99116C17.0492 9.99116 18.4298 11.3719 18.4298 13.0757C18.4298 13.4811 18.1008 13.8101 17.6954 13.8101C17.29 13.8101 16.9611 13.4811 16.9611 13.0757C16.9611 12.1826 16.2384 11.46 15.3453 11.46C14.4523 11.46 13.7297 12.1826 13.7297 13.0757C13.7297 13.4811 13.4006 13.8101 12.9953 13.8101C12.5899 13.8101 12.2609 13.4811 12.2609 13.0757C12.2608 11.3719 13.6415 9.99116 15.3454 9.99116ZM15.3454 7.64111C18.3476 7.64111 20.7799 10.0735 20.7799 13.0757C20.7799 13.4811 20.451 13.8101 20.0455 13.8101C19.6401 13.8101 19.3111 13.4811 19.3111 13.0757C19.3111 10.8842 17.5368 9.10997 15.3454 9.10997C13.154 9.10997 11.3796 10.8842 11.3796 13.0757C11.3796 13.4811 11.0505 13.8101 10.6452 13.8101C10.2398 13.8101 9.91081 13.4811 9.91081 13.0757C9.91081 10.0735 12.3429 7.64111 15.3454 7.64111Z" fill="url(#paint2_linear_201_2895)"/> |
| | | </g> |
| | | </g> |
| | | <g id="Group 2136637016"> |
| | | <g id="路径"> |
| | | <g filter="url(#filter1_i_201_2895)"> |
| | | <path fill-rule="evenodd" clip-rule="evenodd" d="M14.959 36C12.4796 36 10.4713 36.8438 10.4713 37.875C10.4713 38.9062 12.4796 39.75 14.959 39.75C17.4385 39.75 19.4468 38.9062 19.4468 37.875C19.4468 36.8438 17.4385 36 14.959 36Z" fill="#0A3BFF" fill-opacity="0.588235"/> |
| | | </g> |
| | | <path fill-rule="evenodd" clip-rule="evenodd" d="M14.959 36C12.4796 36 10.4713 36.8438 10.4713 37.875C10.4713 38.9062 12.4796 39.75 14.959 39.75C17.4385 39.75 19.4468 38.9062 19.4468 37.875C19.4468 36.8438 17.4385 36 14.959 36Z" stroke="#518EFF" stroke-width="0.4"/> |
| | | </g> |
| | | <path id="路径_2" opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M14.9591 33.75C6.6942 33.75 0 35.598 0 37.875C0 40.152 6.6942 42 14.9591 42C23.224 42 29.9182 40.152 29.9182 37.875C29.9182 35.598 23.224 33.75 14.9591 33.75V33.75Z" stroke="#518EFF" stroke-width="0.6"/> |
| | | <path id="路径_3" fill-rule="evenodd" clip-rule="evenodd" d="M14.9591 35.25C9.58694 35.25 5.23572 36.4225 5.23572 37.875C5.23572 39.3275 9.58694 40.5 14.9591 40.5C20.3313 40.5 24.6825 39.3275 24.6825 37.875C24.6825 36.4225 20.3313 35.25 14.9591 35.25V35.25Z" stroke="#518EFF" stroke-width="0.6"/> |
| | | <path id="路径_4" fill-rule="evenodd" clip-rule="evenodd" d="M14.2111 31.5V37.5H15.707V31.5H14.2111Z" fill="url(#paint3_linear_201_2895)"/> |
| | | </g> |
| | | </g> |
| | | <defs> |
| | | <filter id="filter0_i_201_2895" x="0.0408936" y="0" width="29.9591" height="29.9591" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
| | | <feFlood flood-opacity="0" result="BackgroundImageFix"/> |
| | | <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/> |
| | | <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> |
| | | <feOffset/> |
| | | <feGaussianBlur stdDeviation="4.5"/> |
| | | <feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/> |
| | | <feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/> |
| | | <feBlend mode="overlay" in2="shape" result="effect1_innerShadow_201_2895"/> |
| | | </filter> |
| | | <filter id="filter1_i_201_2895" x="10.2714" y="35.8" width="9.37537" height="5.1499" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
| | | <feFlood flood-opacity="0" result="BackgroundImageFix"/> |
| | | <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/> |
| | | <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> |
| | | <feOffset dy="1"/> |
| | | <feGaussianBlur stdDeviation="0.5"/> |
| | | <feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/> |
| | | <feColorMatrix type="matrix" values="0 0 0 0 0.005478 0 0 0 0 0.0928216 0 0 0 0 0.379808 0 0 0 0.239216 0"/> |
| | | <feBlend mode="normal" in2="shape" result="effect1_innerShadow_201_2895"/> |
| | | </filter> |
| | | <linearGradient id="paint0_linear_201_2895" x1="15.0204" y1="0" x2="15.0204" y2="29.9591" gradientUnits="userSpaceOnUse"> |
| | | <stop stop-color="white" stop-opacity="0"/> |
| | | <stop offset="1" stop-color="white"/> |
| | | </linearGradient> |
| | | <linearGradient id="paint1_linear_201_2895" x1="15.0204" y1="0" x2="15.0204" y2="29.9591" gradientUnits="userSpaceOnUse"> |
| | | <stop stop-color="white" stop-opacity="0"/> |
| | | <stop offset="1" stop-color="white"/> |
| | | </linearGradient> |
| | | <linearGradient id="paint2_linear_201_2895" x1="15.2892" y1="7.64111" x2="15.2892" y2="21.7416" gradientUnits="userSpaceOnUse"> |
| | | <stop stop-color="white"/> |
| | | <stop offset="1" stop-color="#A2F2FB"/> |
| | | </linearGradient> |
| | | <linearGradient id="paint3_linear_201_2895" x1="17.2029" y1="37.5" x2="17.2029" y2="31.5" gradientUnits="userSpaceOnUse"> |
| | | <stop stop-color="#0A3BFF" stop-opacity="0.01"/> |
| | | <stop offset="1" stop-color="#0A3BFF"/> |
| | | </linearGradient> |
| | | </defs> |
| | | </svg> |
| New file |
| | |
| | | <svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| | | <g id="Group 2136637030"> |
| | | <g id="Ellipse 120"> |
| | | <g filter="url(#filter0_i_205_3058)"> |
| | | <circle cx="15" cy="15" r="15" fill="#38100A"/> |
| | | <circle cx="15" cy="15" r="15" fill="url(#paint0_linear_205_3058)" style="mix-blend-mode:overlay"/> |
| | | <circle cx="15" cy="15" r="15" fill="url(#paint1_linear_205_3058)" style="mix-blend-mode:overlay"/> |
| | | </g> |
| | | <circle cx="15" cy="15" r="14.5" stroke="white" style="mix-blend-mode:overlay"/> |
| | | </g> |
| | | <g id="Frame" clip-path="url(#clip0_205_3058)"> |
| | | <g id="Group 1321318248"> |
| | | <path id="Vector" d="M24.5173 11.5642C24.5173 11.3706 22.2846 11.2128 21.1002 11.1927V11.0749C21.1002 10.9977 21.0731 10.9237 21.0248 10.8691C20.9766 10.8145 20.9111 10.7838 20.8429 10.7838C20.7747 10.7838 20.7092 10.8145 20.661 10.8691C20.6128 10.9237 20.5856 10.9977 20.5856 11.0749V11.1927C19.4013 11.2128 17.1686 11.3706 17.1686 11.5642C17.1686 11.7585 19.4013 11.9163 20.5856 11.9365V13.1597H17.7198C17.5046 12.786 17.1361 12.5313 16.7065 12.5313H13.735C13.3054 12.5313 12.937 12.786 12.7217 13.1597H9.85593V11.9364C11.0403 11.9163 13.273 11.7585 13.273 11.5642C13.273 11.3706 11.0403 11.2127 9.85593 11.1926V11.0749C9.85593 10.9977 9.82883 10.9237 9.78058 10.8691C9.73233 10.8145 9.66689 10.7838 9.59866 10.7838C9.53043 10.7838 9.46499 10.8145 9.41674 10.8691C9.3685 10.9237 9.34139 10.9977 9.34139 11.0749V11.1926C8.15702 11.2127 5.92432 11.3706 5.92432 11.5642C5.92432 11.7585 8.15702 11.9163 9.34139 11.9365V13.1597H8.47017C8.14381 13.1597 8.07472 13.6794 8.38594 13.7889L11.8782 15.2562L10.5511 18.2921C10.4722 18.4763 10.4543 18.6865 10.5008 18.8844C10.5473 19.0823 10.6648 19.1999 10.8044 19.3702C11.1129 19.7465 11.6502 19.9449 11.6502 19.9449C11.8782 20.104 12.068 20.0383 12.1534 19.8333C12.2251 19.6612 12.1546 19.4577 12.0381 19.3702L11.4005 18.8852C11.3548 18.8495 11.3219 18.7965 11.3076 18.7358C11.2934 18.675 11.2988 18.6105 11.3228 18.5539L12.6151 15.5043L12.6835 15.5282L12.9112 16.9647C12.92 17.0479 12.9471 17.1272 12.99 17.1955C13.0328 17.2638 13.0901 17.3188 13.1567 17.3556L15.0273 18.386C15.0875 18.4194 15.1537 18.4368 15.2207 18.4368C15.2878 18.4368 15.354 18.4194 15.4142 18.386L17.2849 17.3555C17.3514 17.3187 17.4087 17.2637 17.4516 17.1954C17.4944 17.1271 17.5215 17.0478 17.5303 16.9646L17.7579 15.5281L17.8264 15.5042L19.1187 18.5538C19.1429 18.6103 19.1484 18.6747 19.1342 18.7354C19.1199 18.7961 19.0868 18.849 19.041 18.8844L18.3593 19.4159C18.237 19.5113 18.1613 19.7578 18.2497 19.9449C18.3642 20.1871 18.8003 20.0514 18.9369 19.9449L19.727 19.4159C19.8764 19.3003 19.9841 19.128 20.0307 18.9302C20.0773 18.7324 20.0597 18.5222 19.981 18.3378L18.6581 15.2225L22.0555 13.7889C22.3668 13.6794 22.2977 13.1597 21.9713 13.1597H21.1001V11.9364C22.2845 11.9163 24.5173 11.7585 24.5173 11.5642ZM15.2207 17.1954C14.5733 17.1954 14.0587 16.6417 14.0587 16.0131C14.0587 15.432 14.579 14.851 15.2208 14.851C15.8626 14.851 16.3964 15.3714 16.3828 16.0131C16.3695 16.6416 15.8681 17.1954 15.2207 17.1954Z" fill="url(#paint2_linear_205_3058)"/> |
| | | <path id="Vector_2" d="M14.6398 16.013C14.6398 16.0893 14.6548 16.1649 14.684 16.2354C14.7132 16.3059 14.756 16.3699 14.8099 16.4239C14.8639 16.4778 14.9279 16.5206 14.9984 16.5498C15.0689 16.579 15.1445 16.5941 15.2208 16.5941C15.2971 16.5941 15.3727 16.579 15.4432 16.5498C15.5137 16.5206 15.5777 16.4778 15.6317 16.4239C15.6856 16.3699 15.7284 16.3059 15.7576 16.2354C15.7868 16.1649 15.8018 16.0893 15.8018 16.013C15.8018 15.9367 15.7868 15.8612 15.7576 15.7907C15.7284 15.7202 15.6856 15.6561 15.6317 15.6022C15.5777 15.5482 15.5137 15.5054 15.4432 15.4762C15.3727 15.447 15.2971 15.432 15.2208 15.432C15.1445 15.432 15.0689 15.447 14.9984 15.4762C14.9279 15.5054 14.8639 15.5482 14.8099 15.6022C14.756 15.6561 14.7132 15.7202 14.684 15.7907C14.6548 15.8612 14.6398 15.9367 14.6398 16.013Z" fill="url(#paint3_linear_205_3058)"/> |
| | | </g> |
| | | </g> |
| | | </g> |
| | | <defs> |
| | | <filter id="filter0_i_205_3058" x="0" y="0" width="30" height="30" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
| | | <feFlood flood-opacity="0" result="BackgroundImageFix"/> |
| | | <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/> |
| | | <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> |
| | | <feOffset/> |
| | | <feGaussianBlur stdDeviation="4.5"/> |
| | | <feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/> |
| | | <feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/> |
| | | <feBlend mode="overlay" in2="shape" result="effect1_innerShadow_205_3058"/> |
| | | </filter> |
| | | <linearGradient id="paint0_linear_205_3058" x1="15" y1="0" x2="15" y2="30" gradientUnits="userSpaceOnUse"> |
| | | <stop stop-color="white" stop-opacity="0"/> |
| | | <stop offset="1" stop-color="white"/> |
| | | </linearGradient> |
| | | <linearGradient id="paint1_linear_205_3058" x1="15" y1="0" x2="15" y2="30" gradientUnits="userSpaceOnUse"> |
| | | <stop stop-color="white" stop-opacity="0"/> |
| | | <stop offset="1" stop-color="white"/> |
| | | </linearGradient> |
| | | <linearGradient id="paint2_linear_205_3058" x1="15.2208" y1="20.0803" x2="15.2208" y2="10.7838" gradientUnits="userSpaceOnUse"> |
| | | <stop stop-color="#FCFEFF"/> |
| | | <stop offset="1" stop-color="#FBB6A5"/> |
| | | </linearGradient> |
| | | <linearGradient id="paint3_linear_205_3058" x1="15.2208" y1="16.5941" x2="15.2208" y2="15.432" gradientUnits="userSpaceOnUse"> |
| | | <stop stop-color="#FCFEFF"/> |
| | | <stop offset="1" stop-color="#FBB6A5"/> |
| | | </linearGradient> |
| | | <clipPath id="clip0_205_3058"> |
| | | <rect width="18.75" height="18.75" fill="white" transform="matrix(-1 0 0 1 24.6997 5.44312)"/> |
| | | </clipPath> |
| | | </defs> |
| | | </svg> |
| New file |
| | |
| | | <svg width="30" height="42" viewBox="0 0 30 42" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| | | <g id="Group 2136637029"> |
| | | <g id="Group 2136636871"> |
| | | <g id="Ellipse 115"> |
| | | <g filter="url(#filter0_i_205_3029)"> |
| | | <circle cx="15.0204" cy="14.9795" r="14.9795" fill="#002624"/> |
| | | <circle cx="15.0204" cy="14.9795" r="14.9795" fill="url(#paint0_linear_205_3029)" style="mix-blend-mode:overlay"/> |
| | | <circle cx="15.0204" cy="14.9795" r="14.9795" fill="url(#paint1_linear_205_3029)" style="mix-blend-mode:overlay"/> |
| | | </g> |
| | | <circle cx="15.0204" cy="14.9795" r="14.4795" stroke="white" style="mix-blend-mode:overlay"/> |
| | | </g> |
| | | <g id="Frame"> |
| | | <g id="Frame_2" clip-path="url(#clip0_205_3029)"> |
| | | <g id="Frame_3" clip-path="url(#clip1_205_3029)"> |
| | | <path id="Vector" d="M14.9991 16.3575L10.7982 21.959H19.2L14.9991 16.3575ZM20.1788 9.3562L19.1438 10.2984C20.0832 11.3285 20.6029 12.673 20.6007 14.0671C20.6027 15.461 20.083 16.805 19.1438 17.8349L20.1797 18.7762C21.3533 17.4889 22.0027 15.8091 22.0004 14.0671C22.0004 12.2531 21.3104 10.5993 20.1788 9.3562ZM9.81942 9.3562C8.64503 10.6438 7.9952 12.3244 7.99786 14.0671C7.99525 15.809 8.64435 17.4887 9.81755 18.7762L10.8544 17.834C9.91548 16.8043 9.39579 15.4606 9.39755 14.0671C9.39555 12.6733 9.91527 11.3292 10.8544 10.2993L9.81849 9.3562H9.81942ZM18.1069 11.2406L17.071 12.1828C17.5397 12.6984 17.8004 13.3696 17.7994 14.0662C17.7994 14.7918 17.5238 15.4528 17.071 15.9506L18.1079 16.8928C18.812 16.1203 19.2016 15.1124 19.2 14.0671C19.2 12.9778 18.7857 11.9859 18.1079 11.2406H18.1069ZM11.8904 11.2406C11.186 12.0133 10.7964 13.0216 10.7982 14.0671C10.7965 15.1121 11.1861 16.1198 11.8904 16.8918L12.9263 15.9506C12.4575 15.435 12.1979 14.7637 12.1988 14.0662C12.1988 13.3415 12.4744 12.6806 12.9263 12.1837L11.8904 11.2415V11.2406ZM14.9991 12.6675C14.8122 12.6626 14.6262 12.6953 14.4521 12.7634C14.278 12.8316 14.1193 12.934 13.9854 13.0645C13.8515 13.195 13.745 13.351 13.6724 13.5233C13.5997 13.6955 13.5623 13.8806 13.5623 14.0676C13.5623 14.2546 13.5997 14.4397 13.6724 14.612C13.745 14.7842 13.8515 14.9402 13.9854 15.0707C14.1193 15.2012 14.278 15.3036 14.4521 15.3718C14.6262 15.44 14.8122 15.4726 14.9991 15.4678C15.3642 15.4583 15.7111 15.3067 15.966 15.0451C16.2209 14.7836 16.3635 14.4328 16.3635 14.0676C16.3635 13.7024 16.2209 13.3517 15.966 13.0901C15.7111 12.8286 15.3642 12.6769 14.9991 12.6675Z" fill="url(#paint2_linear_205_3029)"/> |
| | | </g> |
| | | </g> |
| | | </g> |
| | | </g> |
| | | <g id="Group 2136637016"> |
| | | <g id="路径"> |
| | | <g filter="url(#filter1_i_205_3029)"> |
| | | <path fill-rule="evenodd" clip-rule="evenodd" d="M14.959 36C12.4796 36 10.4713 36.8438 10.4713 37.875C10.4713 38.9062 12.4796 39.75 14.959 39.75C17.4385 39.75 19.4468 38.9062 19.4468 37.875C19.4468 36.8438 17.4385 36 14.959 36Z" fill="#159DAD" fill-opacity="0.588235"/> |
| | | </g> |
| | | <path fill-rule="evenodd" clip-rule="evenodd" d="M14.959 36C12.4796 36 10.4713 36.8438 10.4713 37.875C10.4713 38.9062 12.4796 39.75 14.959 39.75C17.4385 39.75 19.4468 38.9062 19.4468 37.875C19.4468 36.8438 17.4385 36 14.959 36Z" stroke="#01DEF7" stroke-width="0.4"/> |
| | | </g> |
| | | <path id="路径_2" opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M14.9591 33.75C6.6942 33.75 0 35.598 0 37.875C0 40.152 6.6942 42 14.9591 42C23.224 42 29.9182 40.152 29.9182 37.875C29.9182 35.598 23.224 33.75 14.9591 33.75V33.75Z" stroke="#01DEF7" stroke-width="0.6"/> |
| | | <path id="路径_3" fill-rule="evenodd" clip-rule="evenodd" d="M14.9591 35.25C9.58694 35.25 5.23572 36.4225 5.23572 37.875C5.23572 39.3275 9.58694 40.5 14.9591 40.5C20.3313 40.5 24.6825 39.3275 24.6825 37.875C24.6825 36.4225 20.3313 35.25 14.9591 35.25V35.25Z" stroke="#01DEF7" stroke-width="0.6"/> |
| | | <path id="路径_4" fill-rule="evenodd" clip-rule="evenodd" d="M14.2111 31.5V37.5H15.707V31.5H14.2111Z" fill="url(#paint3_linear_205_3029)"/> |
| | | </g> |
| | | </g> |
| | | <defs> |
| | | <filter id="filter0_i_205_3029" x="0.0408936" y="0" width="29.9591" height="29.9591" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
| | | <feFlood flood-opacity="0" result="BackgroundImageFix"/> |
| | | <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/> |
| | | <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> |
| | | <feOffset/> |
| | | <feGaussianBlur stdDeviation="4.5"/> |
| | | <feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/> |
| | | <feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/> |
| | | <feBlend mode="overlay" in2="shape" result="effect1_innerShadow_205_3029"/> |
| | | </filter> |
| | | <filter id="filter1_i_205_3029" x="10.2714" y="35.8" width="9.37537" height="5.1499" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
| | | <feFlood flood-opacity="0" result="BackgroundImageFix"/> |
| | | <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/> |
| | | <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> |
| | | <feOffset dy="1"/> |
| | | <feGaussianBlur stdDeviation="0.5"/> |
| | | <feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/> |
| | | <feColorMatrix type="matrix" values="0 0 0 0 0.005478 0 0 0 0 0.0928216 0 0 0 0 0.379808 0 0 0 0.239216 0"/> |
| | | <feBlend mode="normal" in2="shape" result="effect1_innerShadow_205_3029"/> |
| | | </filter> |
| | | <linearGradient id="paint0_linear_205_3029" x1="15.0204" y1="0" x2="15.0204" y2="29.9591" gradientUnits="userSpaceOnUse"> |
| | | <stop stop-color="white" stop-opacity="0"/> |
| | | <stop offset="1" stop-color="white"/> |
| | | </linearGradient> |
| | | <linearGradient id="paint1_linear_205_3029" x1="15.0204" y1="0" x2="15.0204" y2="29.9591" gradientUnits="userSpaceOnUse"> |
| | | <stop stop-color="white" stop-opacity="0"/> |
| | | <stop offset="1" stop-color="white"/> |
| | | </linearGradient> |
| | | <linearGradient id="paint2_linear_205_3029" x1="14.9991" y1="21.959" x2="14.9991" y2="9.3562" gradientUnits="userSpaceOnUse"> |
| | | <stop stop-color="#ABF3FB"/> |
| | | <stop offset="1" stop-color="#FCFEFF"/> |
| | | </linearGradient> |
| | | <linearGradient id="paint3_linear_205_3029" x1="17.2029" y1="37.5" x2="17.2029" y2="31.5" gradientUnits="userSpaceOnUse"> |
| | | <stop stop-color="#01DEF7" stop-opacity="0.01"/> |
| | | <stop offset="1" stop-color="#01DEF7"/> |
| | | </linearGradient> |
| | | <clipPath id="clip0_205_3029"> |
| | | <rect width="15" height="15" fill="white" transform="translate(7.5 8.25)"/> |
| | | </clipPath> |
| | | <clipPath id="clip1_205_3029"> |
| | | <rect width="15" height="15" fill="white" transform="matrix(-1 0 0 1 22.5 8.25)"/> |
| | | </clipPath> |
| | | </defs> |
| | | </svg> |
| New file |
| | |
| | | <svg width="40" height="56" viewBox="0 0 40 56" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| | | <g id="Group 2136637022"> |
| | | <g id="Group 2136637020"> |
| | | <path id="路径" opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M20 45.7333C8.95 45.7333 0 48.033 0 50.8666C0 53.7002 8.95 55.9999 20 55.9999C31.05 55.9999 40 53.7002 40 50.8666C40 48.033 31.05 45.7333 20 45.7333V45.7333Z" stroke="#1EF9FD" stroke-width="0.6"/> |
| | | <path id="路径_2" fill-rule="evenodd" clip-rule="evenodd" d="M20 48C12.8175 48 7 49.34 7 51C7 52.66 12.8175 54 20 54C27.1825 54 33 52.66 33 51C33 49.34 27.1825 48 20 48V48Z" stroke="#1EF9FD" stroke-width="0.6"/> |
| | | <path id="路径_3" fill-rule="evenodd" clip-rule="evenodd" d="M19 42V50H21V42H19Z" fill="url(#paint0_linear_201_2906)"/> |
| | | <g id="路径_4"> |
| | | <g filter="url(#filter0_i_201_2906)"> |
| | | <path fill-rule="evenodd" clip-rule="evenodd" d="M20 49C16.685 49 14 49.9 14 51C14 52.1 16.685 53 20 53C23.315 53 26 52.1 26 51C26 49.9 23.315 49 20 49Z" fill="#3DB4C2" fill-opacity="0.588235"/> |
| | | </g> |
| | | <path fill-rule="evenodd" clip-rule="evenodd" d="M20 49C16.685 49 14 49.9 14 51C14 52.1 16.685 53 20 53C23.315 53 26 52.1 26 51C26 49.9 23.315 49 20 49Z" stroke="#02E3FC" stroke-width="0.4"/> |
| | | </g> |
| | | </g> |
| | | <g id="Frame 2136637015"> |
| | | <g id="Group 2136637019"> |
| | | <g id="Group 2136637018"> |
| | | <g id="Group 2136637017"> |
| | | <path id="形状_1" fill-rule="evenodd" clip-rule="evenodd" d="M21.1865 37.8053C20.8879 37.9788 20.5662 38.0656 20.2213 38.0656C19.8764 38.0656 19.5547 37.9788 19.2562 37.8053L5.40778 29.76C5.10909 29.5861 4.87354 29.349 4.70118 29.0486C4.52881 28.7482 4.44263 28.4247 4.44263 28.078V11.9873C4.44256 11.6406 4.52875 11.317 4.70114 11.0166C4.87351 10.7163 5.10906 10.4792 5.40778 10.3056L19.2562 2.26025C19.5547 2.08674 19.8764 2 20.2213 2C20.5662 2 20.8879 2.08674 21.1865 2.26025L35.0349 10.3056C35.3335 10.4792 35.569 10.7161 35.7414 11.0165C35.9138 11.3169 36 11.6405 36 11.9873V28.0793C35.9997 28.4258 35.9134 28.7491 35.741 29.0493C35.5687 29.3494 35.3333 29.5863 35.0349 29.76L21.1865 37.8053Z" fill="url(#paint1_linear_201_2906)" stroke="url(#paint2_linear_201_2906)" stroke-width="3"/> |
| | | <path id="形状_1_拷贝_2" opacity="0.3" fill-rule="evenodd" clip-rule="evenodd" d="M21.3504 35.512C21.0899 35.6631 20.8091 35.7387 20.5082 35.7387C20.2072 35.7387 19.9264 35.6631 19.666 35.512L7.57987 28.5045C7.31925 28.3533 7.11374 28.1469 6.96331 27.8854C6.81289 27.6239 6.73767 27.3422 6.73767 27.0403V13.0253C6.7377 12.7234 6.81291 12.4417 6.96334 12.1801C7.11377 11.9186 7.31928 11.7123 7.57987 11.5611L19.666 4.55336C19.9265 4.4023 20.2072 4.32678 20.5082 4.32678C20.8091 4.32678 21.0898 4.4023 21.3504 4.55336L33.4364 11.5611C33.6971 11.7123 33.9026 11.9187 34.053 12.1802C34.2034 12.4417 34.2787 12.7234 34.2787 13.0253V27.0403C34.2786 27.3422 34.2034 27.6239 34.053 27.8854C33.9026 28.147 33.697 28.3533 33.4364 28.5045L21.3504 35.512Z" fill="url(#paint3_linear_201_2906)"/> |
| | | </g> |
| | | </g> |
| | | <g id="Group 2136637018_2"> |
| | | <g id="Group 2136637017_2"> |
| | | <path id="Vector" d="M23.3913 19.6115C23.7642 19.9799 24.3236 19.9799 24.6966 19.6115L27.4937 16.8484C27.8667 16.48 27.8667 15.9273 27.4937 15.5589L24.6966 12.7958C24.3236 12.4274 23.7642 12.4274 23.3913 12.7958L20.5941 15.5589C20.2212 15.9273 20.2212 16.48 20.5941 16.8484L23.3913 19.6115ZM18.7294 12.7432H13.6146C13.1085 12.7432 12.6823 13.1643 12.6823 13.6642V18.7431C12.6823 19.243 13.1085 19.6641 13.6146 19.6641H18.756C19.2622 19.6641 19.6884 19.243 19.6884 18.7431V13.6642C19.6618 13.1643 19.2355 12.7432 18.7294 12.7432ZM18.7294 20.5851H13.6146C13.1085 20.5851 12.6823 21.0062 12.6823 21.5061V26.585C12.6823 27.085 13.1085 27.506 13.6146 27.506H18.756C19.2622 27.506 19.6884 27.085 19.6884 26.585V21.5061C19.6618 21.0062 19.2355 20.5851 18.7294 20.5851ZM26.6679 20.5851H21.5265C21.0204 20.5851 20.5941 21.0062 20.5941 21.5061V26.585C20.5941 27.085 21.0204 27.506 21.5265 27.506H26.6679C27.1741 27.506 27.6003 27.085 27.6003 26.585V21.5061C27.6003 21.0062 27.1741 20.5851 26.6679 20.5851Z" fill="url(#paint4_linear_201_2906)"/> |
| | | </g> |
| | | </g> |
| | | </g> |
| | | </g> |
| | | </g> |
| | | <defs> |
| | | <filter id="filter0_i_201_2906" x="13.8" y="48.8" width="12.3999" height="5.3999" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
| | | <feFlood flood-opacity="0" result="BackgroundImageFix"/> |
| | | <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/> |
| | | <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> |
| | | <feOffset dy="1"/> |
| | | <feGaussianBlur stdDeviation="0.5"/> |
| | | <feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/> |
| | | <feColorMatrix type="matrix" values="0 0 0 0 0.005478 0 0 0 0 0.0928216 0 0 0 0 0.379808 0 0 0 0.239216 0"/> |
| | | <feBlend mode="normal" in2="shape" result="effect1_innerShadow_201_2906"/> |
| | | </filter> |
| | | <linearGradient id="paint0_linear_201_2906" x1="23" y1="50" x2="23" y2="42" gradientUnits="userSpaceOnUse"> |
| | | <stop stop-color="#02E3FC" stop-opacity="0.01"/> |
| | | <stop offset="1" stop-color="#01F7F7"/> |
| | | </linearGradient> |
| | | <linearGradient id="paint1_linear_201_2906" x1="35.9969" y1="38.0655" x2="35.9969" y2="2.00703" gradientUnits="userSpaceOnUse"> |
| | | <stop stop-color="#08393C"/> |
| | | <stop offset="0.994" stop-color="#007874"/> |
| | | <stop offset="1" stop-color="#007874"/> |
| | | </linearGradient> |
| | | <linearGradient id="paint2_linear_201_2906" x1="35.9969" y1="38.0655" x2="35.9969" y2="2.00703" gradientUnits="userSpaceOnUse"> |
| | | <stop stop-color="#1A6B6C"/> |
| | | <stop offset="1" stop-color="#C3FFFE"/> |
| | | </linearGradient> |
| | | <linearGradient id="paint3_linear_201_2906" x1="34.2787" y1="35.7411" x2="34.2787" y2="4.32907" gradientUnits="userSpaceOnUse"> |
| | | <stop stop-color="#0C656E" stop-opacity="0.01"/> |
| | | <stop offset="1"/> |
| | | </linearGradient> |
| | | <linearGradient id="paint4_linear_201_2906" x1="20.2278" y1="12.5195" x2="20.2278" y2="27.506" gradientUnits="userSpaceOnUse"> |
| | | <stop stop-color="#4AB8B5"/> |
| | | <stop offset="1" stop-color="#DAFAFB"/> |
| | | </linearGradient> |
| | | </defs> |
| | | </svg> |
| | |
| | | <basic-container> |
| | | <el-form ref="queryParamsRef" :model="searchParams" class="ztzf-page-history-search"> |
| | | <el-form-item label="设备名称" prop="deviceName"> |
| | | <el-input class="ztzf-data-cockpit-search-input" v-model="searchParams.deviceName" placeholder="请输入" |
| | | clearable @clear="handleSearch" /> |
| | | <el-input |
| | | class="ztzf-data-cockpit-search-input" |
| | | v-model="searchParams.deviceName" |
| | | placeholder="请输入" |
| | | clearable |
| | | @clear="handleSearch" |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="设备类型" prop="deviceType"> |
| | | <el-select class="ztzf-data-cockpit-select" popper-class="ztzf-data-cockpit-select-popper" |
| | | v-model="searchParams.deviceType" placeholder="请选择" clearable @change="handleSearch"> |
| | | <el-option v-for="item in dictObj.deviceType" :key="item.dictKey" :label="item.dictValue" |
| | | :value="item.dictKey" /> |
| | | <el-select |
| | | class="ztzf-data-cockpit-select" |
| | | popper-class="ztzf-data-cockpit-select-popper" |
| | | v-model="searchParams.deviceType" |
| | | placeholder="请选择" |
| | | clearable |
| | | @change="handleSearch" |
| | | > |
| | | <el-option |
| | | v-for="item in dictObj.deviceType" |
| | | :key="item.dictKey" |
| | | :label="item.dictValue" |
| | | :value="item.dictKey" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="所属部门" prop="belongDept"> |
| | | <el-tree-select class="ztzf-data-cockpit-select" popper-class="ztzf-data-cockpit-tree-select-popper" |
| | | v-model="searchParams.belongDept" :data="deptTree" :props="treeProps" node-key="id" check-strictly |
| | | clearable @change="handleSearch" /> |
| | | <el-tree-select |
| | | class="ztzf-data-cockpit-select" |
| | | popper-class="ztzf-data-cockpit-tree-select-popper" |
| | | v-model="searchParams.belongDept" |
| | | :data="deptTree" |
| | | :props="treeProps" |
| | | node-key="id" |
| | | check-strictly |
| | | clearable |
| | | @change="handleSearch" |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item class="history-search-actions"> |
| | |
| | | <el-table-column prop="source" show-overflow-tooltip label="来源" /> |
| | | <el-table-column prop="belongDeptName" show-overflow-tooltip label="所属部门" /> |
| | | <el-table-column prop="charger" show-overflow-tooltip label="负责人" /> |
| | | <el-table-column prop="scrapTime" show-overflow-tooltip label="报废时间" /> |
| | | <el-table-column prop="scrapTime" show-overflow-tooltip label="报废时间"> |
| | | <template v-slot="{ row }"> |
| | | {{dayjs(row.scrapTime).format('YYYY-MM-DD')}} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="scrapReason" show-overflow-tooltip label="报废原因" /> |
| | | <el-table-column prop="disposeWay" show-overflow-tooltip label="处置方式" /> |
| | | <el-table-column label="操作" class-name="operation-btns"> |
| | |
| | | </div> |
| | | |
| | | <div class="ztzf-table-pagination"> |
| | | <el-pagination popper-class="ztzf-data-cockpit-select-popper" |
| | | v-model:current-page="searchParams.current" v-model:page-size="searchParams.size" |
| | | layout="total, prev, pager, next, sizes" :total="total" @change="getList" /> |
| | | <el-pagination |
| | | popper-class="ztzf-data-cockpit-select-popper" |
| | | v-model:current-page="searchParams.current" |
| | | v-model:page-size="searchParams.size" |
| | | layout="total, prev, pager, next, sizes" |
| | | :total="total" |
| | | @change="getList" |
| | | /> |
| | | </div> |
| | | </div> |
| | | |
| | |
| | | import FormDiaLog from './FormDiaLog.vue' |
| | | import MaintenanceDiaLog from '@/views/basicManage/maintainRecord/MaintenanceDiaLog.vue' |
| | | import { fwDeviceScrapPageApi } from '@/views/basicManage/deviceStock/fwDeviceScrapApi' |
| | | import dayjs from 'dayjs' |
| | | |
| | | const initSearchParams = () => ({ |
| | | deviceName: '', // 设备名称 |
| | |
| | | provide('planCycleOptions', planCycleOptions) |
| | | |
| | | // 获取列表 |
| | | async function getList () { |
| | | async function getList() { |
| | | loading.value = true |
| | | try { |
| | | const res = await fwDeviceScrapPageApi(searchParams.value) |
| | |
| | | } |
| | | |
| | | // 查询 |
| | | function handleSearch () { |
| | | function handleSearch() { |
| | | searchParams.value.current = 1 |
| | | getList() |
| | | } |
| | | |
| | | // 重置查询 |
| | | function resetForm () { |
| | | function resetForm() { |
| | | queryParamsRef.value?.resetFields() |
| | | searchParams.value.current = 1 |
| | | getList() |
| | | } |
| | | |
| | | // 查看 |
| | | function handleView (row) { |
| | | function handleView(row) { |
| | | dialogRef.value?.open({ mode: 'view', row: { ...row } }) |
| | | } |
| | | |
| | | // 获取字典 |
| | | function getDictList () { |
| | | function getDictList() { |
| | | getDictionaryByCode('deviceType,deviceAtt').then(res => { |
| | | dictObj.value = res.data.data |
| | | }) |
| | | } |
| | | |
| | | // 获取部门树 |
| | | function getDeptTreeFun () { |
| | | function getDeptTreeFun() { |
| | | getDeptTree().then(res => { |
| | | deptTree.value = res.data.data |
| | | }) |
| | | } |
| | | |
| | | // 新增 |
| | | function handleAdd () { |
| | | function handleAdd() { |
| | | dialogRef.value?.open({ mode: 'add' }) |
| | | } |
| | | |
| | | function getPlanCycleLabel (row) { |
| | | function getPlanCycleLabel(row) { |
| | | const item = planCycleOptions.find(option => option.value === row.planCycleType) |
| | | return item?.label + '-' + row.planCycleValue.join(',') |
| | | } |
| | |
| | | <el-form-item label="报废时间" prop="scrapTime"> |
| | | <el-date-picker class="ztzf-data-cockpit-date-picker" popper-class="ztzf-data-cockpit-date-picker-popper" |
| | | v-model="formData.scrapTime" |
| | | type="date" |
| | | type="datetime" |
| | | placeholder="选择日期" |
| | | value-format="YYYY-MM-DD HH:mm:ss" |
| | | clearable |
| | |
| | | @change="handleSearch" |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="时间"> |
| | | <el-date-picker |
| | | popper-class="ztzf-data-cockpit-date-picker-popper" |
| | | class="ztzf-data-cockpit-date-picker" |
| | | v-model="dateRange" |
| | | type="daterange" |
| | | range-separator="至" |
| | | start-placeholder="开始日期" |
| | | end-placeholder="结束日期" |
| | | value-format="YYYY-MM-DD HH:mm:ss" |
| | | @change="handleSearch" |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item class="history-search-actions"> |
| | | <el-button :icon="RefreshRight" @click="resetForm"></el-button> |
| | | <el-button class="search-btn" :icon="Search" @click="handleSearch"></el-button> |
| | |
| | | |
| | | <div class="ztzf-table-toolbar"> |
| | | <el-button :icon="Plus" color="#284FE3" type="primary" @click="handleAdd">设备建档</el-button> |
| | | <el-button :icon="Download" color="#1A2652" type="primary" @click="exportFile" :loading="exportLoading">导出</el-button> |
| | | <!-- <el-button type="danger" :disabled="!selectedIds.length" @click="handleDelete()">删除</el-button>--> |
| | | </div> |
| | | |
| | |
| | | <el-table-column prop="manufacturer" show-overflow-tooltip width="130" label="生产厂商" /> |
| | | <el-table-column prop="createTime" show-overflow-tooltip width="96" label="入库时间"> |
| | | <template v-slot="{ row }"> |
| | | {{ formatDateToSlash(row.createTime) }} |
| | | {{ dayjs(row.createTime).format('YYYY-MM-DD') }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="source" show-overflow-tooltip width="112" label="来源" /> |
| | |
| | | import DeviceChart1 from '@/views/basicManage/deviceStock/DeviceChart1.vue' |
| | | import DeviceChart2 from '@/views/basicManage/deviceStock/DeviceChart2.vue' |
| | | import DeviceChart3 from '@/views/basicManage/deviceStock/DeviceChart3.vue' |
| | | import dayjs from 'dayjs' |
| | | |
| | | const initSearchParams = () => ({ |
| | | deviceName: '', // 设备名称 |
| | |
| | | <template> |
| | | <el-dialog class="ztzf-page-view-dialog" v-model="visible" :title="titleEnum[dialogMode]" @closed="handleClosed" |
| | | destroy-on-close> |
| | | <el-dialog |
| | | class="ztzf-page-view-dialog" |
| | | v-model="visible" |
| | | :title="titleEnum[dialogMode]" |
| | | @closed="handleClosed" |
| | | destroy-on-close |
| | | > |
| | | <div class="detail-container" v-if="dialogReadonly"> |
| | | <div class="detail-title">设备详情</div> |
| | | <el-row> |
| | |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | <el-form class="dialog-form" v-else ref="formRef" :model="formData" :rules="rules" |
| | | :disabled="dialogReadonly" label-width="110px"> |
| | | <el-form |
| | | class="dialog-form" |
| | | v-else |
| | | ref="formRef" |
| | | :model="formData" |
| | | :rules="rules" |
| | | :disabled="dialogReadonly" |
| | | label-width="110px" |
| | | > |
| | | <el-row> |
| | | <el-col :span="24"> |
| | | <el-form-item label="维护内容" prop="maintainContent"> |
| | |
| | | class="ztzf-data-cockpit-date-picker" |
| | | popper-class="ztzf-data-cockpit-date-picker-popper" |
| | | v-model="formData.maintainTime" |
| | | type="date" |
| | | type="datetime" |
| | | placeholder="选择日期" |
| | | value-format="YYYY-MM-DD HH:mm:ss" |
| | | clearable |
| | |
| | | const deviceList = ref([]) // 设备列表 |
| | | |
| | | const dialogReadonly = computed(() => dialogMode.value === 'view') |
| | | const titleEnum = ref({ edit: '编辑', view: '查看', add: '维护计划' }) |
| | | const titleEnum = ref({ edit: '维护', view: '查看', add: '维护计划' }) |
| | | |
| | | const planCycleLabel = computed(() => { |
| | | const item = planCycleOptions.find(option => option.value === formData.value.planCycleType) |
| | |
| | | <template> |
| | | <el-dialog v-model="visible" class="ztzf-page-map-view-dialog" :show-close="false" :close-on-click-modal="false" |
| | | destroy-on-close @closed="handleClosed"> |
| | | <el-dialog |
| | | v-model="visible" |
| | | class="ztzf-page-map-view-dialog" |
| | | :show-close="false" |
| | | :close-on-click-modal="false" |
| | | destroy-on-close |
| | | @closed="handleClosed" |
| | | > |
| | | <div class="dialog-container"> |
| | | <div class="left-container"> |
| | | <div class="leftMap ztzf-cesium" id="detectionRangeMap"></div> |
| | |
| | | <div class="val">{{ formData.deviceModel || '-' }}</div> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <div class="label">覆盖范围</div> |
| | | <div class="label">覆盖范围(m)</div> |
| | | <div class="val">{{ formatRange(formData.effectiveRangeKm) }}</div> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | |
| | | <el-form v-else ref="formRef" class="dialog-form" :model="formData" :rules="rules" label-width="96px"> |
| | | <el-form v-else ref="formRef" class="dialog-form" :model="formData" :rules="rules" label-width="110px"> |
| | | <el-form-item label="侦测设备" prop="deviceId"> |
| | | <el-select class="ztzf-data-cockpit-select" popper-class="ztzf-data-cockpit-select-popper" |
| | | v-model="formData.deviceId" placeholder="请选择" clearable @change="handleDeviceChange"> |
| | | <el-option v-for="item in deviceOptions" :key="item.value" :label="item.label" |
| | | :value="item.value" /> |
| | | <el-select |
| | | class="ztzf-data-cockpit-select" |
| | | popper-class="ztzf-data-cockpit-select-popper" |
| | | v-model="formData.deviceId" |
| | | placeholder="请选择" |
| | | clearable |
| | | @change="handleDeviceChange" |
| | | > |
| | | <el-option v-for="item in deviceOptions" :key="item.value" :label="item.label" :value="item.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="设备类型" prop="deviceType"> |
| | | <el-select class="ztzf-data-cockpit-select" popper-class="ztzf-data-cockpit-select-popper" |
| | | v-model="formData.deviceType" placeholder="请选择" clearable> |
| | | <el-option v-for="item in dictObj.deviceType" :key="item.dictKey" :label="item.dictValue" |
| | | :value="item.dictKey" /> |
| | | <el-select |
| | | class="ztzf-data-cockpit-select" |
| | | popper-class="ztzf-data-cockpit-select-popper" |
| | | v-model="formData.deviceType" |
| | | placeholder="请选择" |
| | | clearable |
| | | > |
| | | <el-option |
| | | v-for="item in dictObj.deviceType" |
| | | :key="item.dictKey" |
| | | :label="item.dictValue" |
| | | :value="item.dictKey" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="设备位置" prop="deployLocation"> |
| | | <el-input class="ztzf-data-cockpit-search-input" v-model="formData.deployLocation" |
| | | placeholder="请选择位置"> |
| | | <el-input |
| | | class="ztzf-data-cockpit-search-input" |
| | | v-model="formData.deployLocation" |
| | | disabled |
| | | placeholder="请选择位置" |
| | | > |
| | | <template #suffix> |
| | | <span class="suffix-action">地图选点</span> |
| | | </template> |
| | | </el-input> |
| | | </el-form-item> |
| | | <el-form-item label="设备编号" prop="deviceSn"> |
| | | <el-input class="ztzf-data-cockpit-search-input" v-model="formData.deviceSn" placeholder="请输入" |
| | | clearable /> |
| | | <el-input |
| | | class="ztzf-data-cockpit-search-input" |
| | | v-model="formData.deviceSn" |
| | | placeholder="请输入" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="设备型号" prop="deviceModel"> |
| | | <el-input class="ztzf-data-cockpit-search-input" v-model="formData.deviceModel" |
| | | placeholder="请输入" clearable /> |
| | | <el-input |
| | | class="ztzf-data-cockpit-search-input" |
| | | v-model="formData.deviceModel" |
| | | placeholder="请输入" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="覆盖范围" prop="effectiveRangeKm"> |
| | | <el-input-number class="ztzf-data-cockpit-search-input" v-model="formData.effectiveRangeKm" :min="0" :precision="2" :controls="false" |
| | | placeholder="请输入" /> |
| | | <el-form-item label="覆盖范围(m)" prop="effectiveRangeKm"> |
| | | <el-input-number |
| | | class="ztzf-data-cockpit-search-input" |
| | | v-model="formData.effectiveRangeKm" |
| | | :min="0" |
| | | :precision="2" |
| | | :controls="false" |
| | | placeholder="请输入" |
| | | /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <div class="footer"> |
| | | <el-button color="#2B2B4C" @click="handleCancel">{{ dialogReadonly ? '关闭' : '取消' }}</el-button> |
| | | <el-button color="#284FE3" v-if="!dialogReadonly" type="primary" :loading="submitting" |
| | | :disabled="submitting" @click="handleSubmit"> |
| | | <el-button |
| | | color="#284FE3" |
| | | v-if="!dialogReadonly" |
| | | type="primary" |
| | | :loading="submitting" |
| | | :disabled="submitting" |
| | | @click="handleSubmit" |
| | | > |
| | | 保存 |
| | | </el-button> |
| | | </div> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, inject, nextTick, ref } from 'vue' |
| | | import { computed, inject, nextTick, ref, watch } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { |
| | | detectionRangeDetailApi, |
| | |
| | | } from '@/api/detectionCountermeasure/detectionRange' |
| | | import { PublicCesium } from '@/utils/cesium/publicCesium' |
| | | import * as Cesium from 'cesium' |
| | | import deviceZcImg from '@/assets/images/dataCockpit/device-zc.svg' |
| | | |
| | | const initForm = () => ({ |
| | | deviceId: '', // 侦测设备ID |
| | |
| | | const emit = defineEmits(['success']) |
| | | const formRef = ref(null) // 表单实例 |
| | | const formData = ref(initForm()) // 表单数据 |
| | | const visible = ref(false) // 弹框显隐 |
| | | const visible = defineModel() // 弹框显隐 |
| | | const dialogMode = ref('add') // 弹框模式 |
| | | const submitting = ref(false) // 提交中 |
| | | const deviceOptions = ref([]) |
| | | const dialogReadonly = computed(() => dialogMode.value === 'view') |
| | | let viewer |
| | | let redPointEntity |
| | | let rangeCircleEntity |
| | | const formTitle = computed(() => { |
| | | if (dialogMode.value === 'edit') { |
| | | return '编辑侦测范围' |
| | |
| | | effectiveRangeKm: [{ required: true, message: '请输入覆盖范围', trigger: ['blur', 'change'] }], |
| | | } |
| | | |
| | | function handleDeviceChange (value) { |
| | | function handleDeviceChange(value) { |
| | | const target = deviceOptions.value.find(item => item.value === value) |
| | | const raw = target?.raw |
| | | if (!raw) return |
| | |
| | | raw.deployLocation ?? raw.address ?? raw.location ?? raw.deployAddress ?? formData.value.deployLocation |
| | | } |
| | | |
| | | function getDeviceTypeLabel (value) { |
| | | function getDeviceTypeLabel(value) { |
| | | const list = dictObj?.deviceType ?? [] |
| | | const target = list.find(item => item.dictKey === value) |
| | | return target?.dictValue ?? value ?? '-' |
| | | } |
| | | |
| | | function formatRange (value) { |
| | | function formatRange(value) { |
| | | if (value == null || value === '') return '-' |
| | | return `${value}km` |
| | | return `${value}` |
| | | } |
| | | |
| | | // 关闭弹框 |
| | | function handleCancel () { |
| | | function handleCancel() { |
| | | visible.value = false |
| | | } |
| | | |
| | | // 提交新增/编辑 |
| | | async function handleSubmit () { |
| | | async function handleSubmit() { |
| | | const isValid = await formRef.value?.validate().catch(() => false) |
| | | if (!isValid) return |
| | | submitting.value = true |
| | |
| | | } |
| | | |
| | | // 加载详情 |
| | | async function loadDetail (id) { |
| | | async function loadDetail(id) { |
| | | if (!id) return null |
| | | const res = await detectionRangeDetailApi({ id }) |
| | | return res?.data?.data ?? null |
| | | } |
| | | |
| | | async function loadDeviceOptions () { |
| | | async function loadDeviceOptions() { |
| | | const res = await detectionRangePageApi({ |
| | | current: 1, |
| | | size: 200, |
| | |
| | | } |
| | | |
| | | // 关闭后重置 |
| | | function handleClosed () { |
| | | function handleClosed() { |
| | | formData.value = initForm() |
| | | clearRangeCircle() |
| | | viewer = null |
| | | redPointEntity = null |
| | | rangeCircleEntity = null |
| | | } |
| | | |
| | | function setMapPoint (longitude, latitude) { |
| | | function setMapPoint(longitude, latitude) { |
| | | if (!viewer || longitude == null || latitude == null) return |
| | | if (!redPointEntity) { |
| | | redPointEntity = viewer.entities.add({ |
| | | position: Cesium.Cartesian3.fromDegrees(longitude, latitude), |
| | | point: { |
| | | color: Cesium.Color.RED, |
| | | pixelSize: 10, |
| | | billboard: { |
| | | image: deviceZcImg, // 这里替换为你的图片路径或变量 deviceZcImg |
| | | width: 30, // 可选:设置图片宽度 |
| | | height: 42, // 可选:设置图片高度 |
| | | verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 设置图片底部对齐坐标点 |
| | | }, |
| | | label: { |
| | | text: `${longitude.toFixed(6)}, ${latitude.toFixed(6)}`, |
| | | font: '14px', |
| | | font: '16px', |
| | | fillColor: Cesium.Color.WHITE, |
| | | backgroundColor: Cesium.Color.BLACK.withAlpha(0.45), |
| | | backgroundPadding: new Cesium.Cartesian2(5, 5), |
| | | pixelOffset: new Cesium.Cartesian2(0, -20), |
| | | showBackground: true, |
| | | style: Cesium.LabelStyle.FILL_AND_OUTLINE, |
| | | horizontalOrigin: Cesium.HorizontalOrigin.CENTER, |
| | | verticalOrigin: Cesium.VerticalOrigin.CENTER, |
| | | outlineWidth: 1, |
| | | outlineColor: Cesium.Color.WHITE, |
| | | pixelOffset: new Cesium.Cartesian2(0, -60), |
| | | eyeOffset: new Cesium.Cartesian3(0, 0, -20), |
| | | }, |
| | | }) |
| | |
| | | redPointEntity.position = Cesium.Cartesian3.fromDegrees(longitude, latitude) |
| | | redPointEntity.label.text = `${longitude.toFixed(6)}, ${latitude.toFixed(6)}` |
| | | } |
| | | updateRangeCircle(longitude, latitude, formData.value.effectiveRangeKm) |
| | | } |
| | | |
| | | function initMap () { |
| | | function parseDeployLocation(value) { |
| | | if (!value) return null |
| | | const [longitude, latitude] = value.split(',').map(item => Number(item.trim())) |
| | | if (Number.isNaN(longitude) || Number.isNaN(latitude)) return null |
| | | return { longitude, latitude } |
| | | } |
| | | |
| | | function clearRangeCircle() { |
| | | if (viewer && rangeCircleEntity) { |
| | | viewer.entities.remove(rangeCircleEntity) |
| | | } |
| | | } |
| | | |
| | | function updateRangeCircle(longitude, latitude, range) { |
| | | if (!viewer || longitude == null || latitude == null) return |
| | | const rangeMeters = Number(range) |
| | | if (!Number.isFinite(rangeMeters) || rangeMeters <= 0) { |
| | | clearRangeCircle() |
| | | rangeCircleEntity = null |
| | | return |
| | | } |
| | | const position = Cesium.Cartesian3.fromDegrees(longitude, latitude) |
| | | if (!rangeCircleEntity) { |
| | | rangeCircleEntity = viewer.entities.add({ |
| | | position, |
| | | ellipse: { |
| | | semiMajorAxis: rangeMeters, |
| | | semiMinorAxis: rangeMeters, |
| | | material: Cesium.Color.fromCssColorString('#6b9ca8').withAlpha(0.6), |
| | | outline: true, |
| | | outlineColor: Cesium.Color.fromCssColorString('#6b9ca8').withAlpha(0.6), |
| | | heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, |
| | | }, |
| | | }) |
| | | return |
| | | } |
| | | rangeCircleEntity.position = position |
| | | rangeCircleEntity.ellipse.semiMajorAxis = rangeMeters |
| | | rangeCircleEntity.ellipse.semiMinorAxis = rangeMeters |
| | | } |
| | | |
| | | function initMap() { |
| | | if (viewer) return |
| | | const publicCesiumInstance = new PublicCesium({ |
| | | dom: 'detectionRangeMap', |
| | |
| | | viewer = publicCesiumInstance.getViewer() |
| | | } |
| | | |
| | | function handleMapClick (click) { |
| | | function handleMapClick(click) { |
| | | const pos = click.position |
| | | const cartesian3 = viewer.camera.pickEllipsoid(pos, viewer.scene.globe.ellipsoid) |
| | | if (!cartesian3) return |
| | |
| | | } |
| | | |
| | | // 打开弹框 |
| | | async function open ({ mode, row } = {}) { |
| | | async function open({ mode, row } = {}) { |
| | | dialogMode.value = mode || 'add' |
| | | visible.value = true |
| | | await nextTick() |
| | | initMap() |
| | | if (dialogMode.value === 'add') { |
| | |
| | | const detail = await loadDetail(row?.id) |
| | | formData.value = detail ?? row |
| | | } |
| | | formData.value.deployLocation = `${formData.value.longitude}, ${formData.value.latitude}` |
| | | formData.value.deviceId = formData.value.id ?? formData.value.deviceId |
| | | await loadDeviceOptions() |
| | | const currentName = formData.value.deviceName |
| | |
| | | } |
| | | const location = formData.value.deployLocation |
| | | if (location) { |
| | | const [longitude, latitude] = location.split(',').map(item => Number(item.trim())) |
| | | if (!Number.isNaN(longitude) && !Number.isNaN(latitude)) { |
| | | setMapPoint(longitude, latitude) |
| | | const parsed = parseDeployLocation(location) |
| | | if (parsed) { |
| | | setMapPoint(parsed.longitude, parsed.latitude) |
| | | viewer.flyTo(redPointEntity, { duration: 0 }) |
| | | } |
| | | } |
| | | } |
| | | |
| | | defineExpose({ open }) |
| | | |
| | | watch( |
| | | () => formData.value.effectiveRangeKm, |
| | | value => { |
| | | const parsed = parseDeployLocation(formData.value.deployLocation) |
| | | if (!parsed) return |
| | | updateRangeCircle(parsed.longitude, parsed.latitude, value) |
| | | } |
| | | ) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"></style> |
| | |
| | | <template> |
| | | <basic-container> |
| | | <el-form ref="queryParamsRef" :model="searchParams" class="ztzf-page-history-search"> |
| | | <el-form-item label="名称" prop="deviceName"> |
| | | <el-form-item label="设备名称" prop="deviceName"> |
| | | <el-input |
| | | class="ztzf-data-cockpit-search-input" |
| | | v-model="searchParams.deviceName" |
| | |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="类型" prop="deviceType"> |
| | | <el-form-item label="设备类型" prop="deviceType"> |
| | | <el-select |
| | | class="ztzf-data-cockpit-select" |
| | | popper-class="ztzf-data-cockpit-select-popper" |
| | |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="deviceModel" show-overflow-tooltip label="型号" /> |
| | | <el-table-column show-overflow-tooltip label="覆盖范围"> |
| | | <el-table-column show-overflow-tooltip label="覆盖范围(m)"> |
| | | <template v-slot="{ row }"> |
| | | {{ formatRange(row) }} |
| | | </template> |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <DetectionRangeDialog ref="dialogRef" @success="getList" /> |
| | | <DetectionRangeDialog ref="dialogRef" @success="getList" v-model="visible" v-if="visible" /> |
| | | </basic-container> |
| | | </template> |
| | | <script setup> |
| | |
| | | const dictObj = ref({ |
| | | deviceType: [], // 设备类型 |
| | | }) |
| | | |
| | | const visible = ref(false) |
| | | provide('dictObj', dictObj) |
| | | |
| | | // 获取列表 |
| | |
| | | |
| | | // 新增 |
| | | function handleAdd() { |
| | | dialogRef.value?.open({ mode: 'add' }) |
| | | visible.value = true |
| | | nextTick(() => { |
| | | dialogRef.value?.open({ mode: 'add' }) |
| | | }) |
| | | } |
| | | |
| | | // 查看 |
| | | function handleView(row) { |
| | | dialogRef.value?.open({ mode: 'view', row: { ...row } }) |
| | | visible.value = true |
| | | nextTick(() => { |
| | | dialogRef.value?.open({ mode: 'view', row: { ...row } }) |
| | | }) |
| | | } |
| | | |
| | | // 编辑 |
| | | function handleEdit(row) { |
| | | dialogRef.value?.open({ mode: 'edit', row: { ...row } }) |
| | | visible.value = true |
| | | nextTick(() => { |
| | | dialogRef.value?.open({ mode: 'edit', row: { ...row } }) |
| | | }) |
| | | } |
| | | |
| | | // 删除 |
| | |
| | | } |
| | | |
| | | function formatDeployLocation(row) { |
| | | if (!row) return '-' |
| | | const location = row.deployLocation ?? row.address ?? row.location ?? row.deployAddress |
| | | if (Array.isArray(location) && location.length) { |
| | | return location.filter(value => value !== null && value !== undefined).join(',') |
| | | } |
| | | if (typeof location === 'string' && location.trim()) { |
| | | return location |
| | | } |
| | | const longitude = row.deployLongitude ?? row.longitude |
| | | const latitude = row.deployLatitude ?? row.latitude |
| | | if (longitude != null || latitude != null) { |
| | | return `${longitude ?? '-'},${latitude ?? '-'}` |
| | | } |
| | | return '-' |
| | | if (!row.longitude || !row.latitude) return '' |
| | | return `${row.longitude}, ${row.latitude}` |
| | | } |
| | | |
| | | function formatRange(row) { |
| | |
| | | </el-row> |
| | | </div> |
| | | |
| | | <el-form v-else ref="formRef" :model="formData" :rules="rules" label-width="120px" class="dialog-form"> |
| | | <el-form v-else ref="formRef" :model="formData" :rules="rules" label-width="120" class="dialog-form"> |
| | | <el-row> |
| | | <el-col :span="24"><h3 style="color: #ffffff">工作模式</h3></el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="起始扇区" prop="startSector"> |
| | | <el-input |
| | |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="24"><h3 style="color: #ffffff">工作模式</h3></el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="伺服模式" prop="servoMode"> |
| | | <el-select |
| | |
| | | <script setup> |
| | | import { computed, ref } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { fwDeviceConfigSubmitApi } from './deviceAppConfigApi' |
| | | import { fwDeviceConfigDetailApi, fwDeviceConfigSubmitApi } from './deviceAppConfigApi' |
| | | import { fieldRules } from '@ztzf/utils' |
| | | |
| | | const configKeys = [ |
| | |
| | | const dialogMode = ref('view') // 弹框模式 |
| | | const submitting = ref(false) // 提交中 |
| | | const readonly = computed(() => dialogMode.value === 'view') |
| | | const titleEnum = ref({ edit: '编辑', view: '查看' }) |
| | | const titleEnum = ref({ edit: '配置接口', view: '查看' }) |
| | | |
| | | const searchModeOptions = [ |
| | | { label: '0', value: '0' }, |
| | |
| | | } |
| | | } |
| | | |
| | | function loadingDetails() { |
| | | fwDeviceConfigDetailApi({ deviceId: formData.value.deviceId }).then(res => { |
| | | const configJson = res.data.data ? JSON.parse(res.data.data.configJson) : {} |
| | | formData.value = { ...formData.value, ...configJson } |
| | | }) |
| | | } |
| | | |
| | | // 打开弹框 |
| | | async function open({ mode, row } = {}) { |
| | | dialogMode.value = mode || 'view' |
| | | formData.value = { ...initForm(), deviceId: row.id } |
| | | loadingDetails() |
| | | } |
| | | |
| | | defineExpose({ open }) |
| | |
| | | <el-table-column prop="manufacturer" show-overflow-tooltip label="厂商" /> |
| | | <el-table-column prop="contactPhone" show-overflow-tooltip label="联系方式" /> |
| | | <el-table-column prop="sceneName" show-overflow-tooltip label="所属场景" /> |
| | | <el-table-column label="设置配置" class-name="operation-btns"> |
| | | <el-table-column label="操作" class-name="operation-btns"> |
| | | <template v-slot="{ row }"> |
| | | <el-link @click="openForm('edit', row)" type="warning">配置接口</el-link> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" class-name="operation-btns"> |
| | | <!-- <el-table-column label="操作" class-name="operation-btns"> |
| | | <template v-slot="{ row }"> |
| | | <el-link @click="handleEdit(row)">编辑</el-link> |
| | | <el-link @click="handleDelete(row)" type="danger">删除</el-link> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table-column>--> |
| | | </el-table> |
| | | </div> |
| | | |
| | |
| | | } |
| | | |
| | | function formatDeployLocation(row) { |
| | | if (!row) return '' |
| | | const longitude = row.deployLongitude ?? row.longitude |
| | | const latitude = row.deployLatitude ?? row.latitude |
| | | if (longitude == null || latitude == null) return '' |
| | | return `${longitude}, ${latitude}` |
| | | if (!row.longitude || !row.latitude) return '' |
| | | return `${row.longitude}, ${row.latitude}` |
| | | } |
| | | |
| | | onMounted(() => { |
| | |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="设备状态" prop="deviceStatus"> |
| | | <el-form-item label="设备状态" prop="status"> |
| | | <el-select |
| | | class="ztzf-data-cockpit-select" |
| | | popper-class="ztzf-data-cockpit-select-popper" |
| | | v-model="searchParams.deviceStatus" |
| | | v-model="searchParams.status" |
| | | placeholder="请选择" |
| | | clearable |
| | | @clear="handleSearch" |
| | |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="设备编号" prop="deviceSn"> |
| | | <el-form-item label="设备编码" prop="deviceSn"> |
| | | <el-input |
| | | class="ztzf-data-cockpit-search-input" |
| | | v-model="searchParams.deviceSn" |
| | |
| | | <el-table-column type="index" show-overflow-tooltip width="64" label="序号" /> |
| | | <el-table-column prop="deviceName" show-overflow-tooltip width="140" label="设备名称" /> |
| | | <el-table-column prop="deviceSn" show-overflow-tooltip width="140" label="设备编码" /> |
| | | <el-table-column prop="deviceStatus" show-overflow-tooltip width="100" label="设备状态"> |
| | | <el-table-column prop="status" show-overflow-tooltip width="100" label="设备状态"> |
| | | <template v-slot="{ row }"> |
| | | {{ getDictLabel(row.deviceStatus, dictObj.deviceStatus) }} |
| | | {{ formatDeployLocation(row) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="areaName" show-overflow-tooltip width="120" label="区域" /> |
| | |
| | | // 初始化查询参数 |
| | | const initSearchParams = () => ({ |
| | | deviceId: '', // 设备名称 |
| | | deviceStatus: '', // 设备状态 |
| | | status: '', // 设备状态 |
| | | deviceSn: '', // 设备编号 |
| | | current: 1, // 当前页 |
| | | size: 10, // 每页大小 |
| | |
| | | } |
| | | return row.dispatchStartTime || row.dispatchEndTime || '' |
| | | } |
| | | |
| | | function formatDeployLocation(row) { |
| | | if (!row.longitude || !row.latitude) return '' |
| | | return `${row.longitude}, ${row.latitude}` |
| | | } |
| | | // 获取列表 |
| | | async function getList() { |
| | | loading.value = true |
| | |
| | | <template> |
| | | <el-dialog class="ztzf-page-map-view-dialog" v-model="visible" :show-close="false" |
| | | :close-on-click-modal="false" @closed="handleClosed" destroy-on-close> |
| | | <el-dialog |
| | | class="ztzf-page-map-view-dialog" |
| | | v-model="visible" |
| | | :show-close="false" |
| | | :close-on-click-modal="false" |
| | | @closed="handleClosed" |
| | | destroy-on-close |
| | | > |
| | | <div class="dialog-container"> |
| | | <div class="left-container"> |
| | | <div class="leftMap ztzf-cesium" id="leftMapContainer"></div> |
| | |
| | | import { PublicCesium } from '@/utils/cesium/publicCesium' |
| | | import * as Cesium from 'cesium' |
| | | import { flyVisual } from '@/utils/cesium/mapUtil' |
| | | import { ArrowLineMaterialProperty } from '@/utils/cesium/Material' |
| | | |
| | | const initForm = () => ({ |
| | | droneName: '', |
| | |
| | | }) |
| | | |
| | | // 关闭弹框 |
| | | function handleCancel () { |
| | | function handleCancel() { |
| | | visible.value = false |
| | | } |
| | | |
| | | // 加载详情 |
| | | async function loadDetail () { |
| | | async function loadDetail() { |
| | | if (!formData.value.id) return |
| | | const res = await fwDroneFlightRecordDetailApi({ id: formData.value.id }) |
| | | const res1 = await fwDroneFlightRecordDetailPageApi({ flightRecordId: formData.value.id }) |
| | |
| | | formData.value = res?.data?.data ?? {} |
| | | } |
| | | |
| | | let arrowLineMaterialProperty = new ArrowLineMaterialProperty({ |
| | | color: new Cesium.Color(128 / 255, 215 / 255, 255 / 255, 1), |
| | | directionColor: new Cesium.Color(1, 1, 1, 1), |
| | | outlineColor: new Cesium.Color(1, 1, 1, 1), |
| | | outlineWidth: 0, |
| | | speed: 5, |
| | | }) |
| | | |
| | | // 角度转c3 |
| | | function degreesToC3 ({ longitude, latitude, height = 0 }) { |
| | | function degreesToC3({ longitude, latitude, height = 0 }) { |
| | | return Cesium.Cartesian3.fromDegrees(longitude, latitude, height) |
| | | } |
| | | // 渲染航线 |
| | | function drawFlightPath () { |
| | | function drawFlightPath() { |
| | | const positions = list.value.map(p => degreesToC3(p)) |
| | | droneLineEntity && geometricSource.entities.remove(droneLineEntity) |
| | | const color = Cesium.Color.fromCssColorString('#00D690') |
| | | droneLineEntity = geometricSource.entities.add({ |
| | | polyline: { |
| | | width: 4, |
| | | positions: positions, |
| | | width: 3, |
| | | material: new Cesium.PolylineOutlineMaterialProperty({ color }), |
| | | material: arrowLineMaterialProperty, |
| | | clampToGround: false, |
| | | }, |
| | | }) |
| | | viewer.flyTo(droneLineEntity) |
| | | viewer.flyTo(droneLineEntity, { duration: 0 }) |
| | | } |
| | | |
| | | // 关闭后重置 |
| | | function handleClosed () { |
| | | function handleClosed() { |
| | | formData.value = initForm() |
| | | list.value = [] |
| | | viewer = null |
| | |
| | | } |
| | | |
| | | let viewer, geometricSource, droneLineEntity |
| | | function initMap () { |
| | | function initMap() { |
| | | if (viewer) return |
| | | const publicCesiumInstance = new PublicCesium({ |
| | | dom: 'leftMapContainer', |
| | |
| | | } |
| | | |
| | | // 打开弹框 |
| | | async function open ({ mode, row } = {}) { |
| | | async function open({ mode, row } = {}) { |
| | | dialogMode.value = mode || 'add' |
| | | visible.value = true |
| | | await nextTick() |
| | |
| | | "@vitejs/plugin-vue": "catalog:", |
| | | "@vue/compiler-sfc": "catalog:", |
| | | "postcss-pxtorem": "catalog:", |
| | | "prettier": "catalog:", |
| | | "sass": "catalog:", |
| | | "terser": "catalog:", |
| | | "unplugin-auto-import": "catalog:", |
| | |
| | | ### |
| | | # @Author : yuan |
| | | # @Date : 2026-01-07 14:58:30 |
| | | # @LastEditors : yuan |
| | | # @LastEditTime : 2026-01-16 11:02:24 |
| | | # @FilePath : \applications\task-work-order\env\.env.development |
| | | # @Description : |
| | | # Copyright 2026 OBKoro1, All Rights Reserved. |
| | | # 2026-01-07 14:58:30 |
| | | ### |
| | | NODE_ENV = 'development' |
| | | |
| | | #开发环境配置 |
| | |
| | | |
| | | #开发环境代理地址(推荐本地新建文件 .env.development.local 来进行覆盖) |
| | | # VITE_APP_URL = https://wrj.shuixiongit.com/api |
| | | VITE_APP_URL= http://192.168.1.33:81 |
| | | VITE_APP_URL= http://192.168.1.204 |
| | | |
| | | #新大屏地址 |
| | | VITE_APP_DASHBOARD_URL = 'https://wrj.shuixiongit.com/command-center-dashboard/' |
| | |
| | | "@vitejs/plugin-vue": "catalog:", |
| | | "@vue/compiler-sfc": "catalog:", |
| | | "postcss-pxtorem": "catalog:", |
| | | "prettier": "catalog:", |
| | | "sass": "catalog:", |
| | | "terser": "catalog:", |
| | | "unplugin-auto-import": "catalog:", |
| New file |
| | |
| | | /* |
| | | * @Author : yuan |
| | | * @Date : 2026-01-09 11:14:04 |
| | | * @LastEditors : yuan |
| | | * @LastEditTime : 2026-01-09 11:35:14 |
| | | * @FilePath : \applications\drone-command\src\api\dataCockpit\index.js |
| | | * @Description : |
| | | * Copyright 2026 OBKoro1, All Rights Reserved. |
| | | * 2026-01-09 11:14:04 |
| | | */ |
| | | import request from '@/axios' |
| | | |
| | | // 设备查询 |
| | | export const deviceSearchApi = (params) => { |
| | | return request({ |
| | | url: '/drone-fw/cockpit/dataCockpit/deviceSearch', |
| | | method: 'get', |
| | | params |
| | | }) |
| | | } |
| | | |
| | | // 历史告警/实时告警 |
| | | export const alarmLogApi = (params) => { |
| | | return request({ |
| | | url: '/drone-fw/cockpit/dataCockpit/alarmLog', |
| | | method: 'get', |
| | | params |
| | | }) |
| | | } |
| | | |
| | | // 告警记录统计 |
| | | export const alarmStatisticsApi = () => { |
| | | return request({ |
| | | url: '/drone-fw/cockpit/dataCockpit/alarmStatistics', |
| | | method: 'get' |
| | | }) |
| | | } |
| | | |
| | | // 信号干扰/诱导驱离 |
| | | export const interferenceAndExpulsionApi = (data) => { |
| | | return request({ |
| | | url: '/drone-fw/cockpit/dataCockpit/interferenceAndExpulsion', |
| | | method: 'post', |
| | | data |
| | | }) |
| | | } |
| | | |
| | | // 设备统计 |
| | | export const deviceStatisticsApi = () => { |
| | | return request({ |
| | | url: '/drone-fw/cockpit/dataCockpit/deviceStatistics', |
| | | method: 'get' |
| | | }) |
| | | } |
| New file |
| | |
| | | <svg width="28" height="48" viewBox="0 0 28 48" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| | | <g id="Vector"> |
| | | <path d="M16.5455 0C16.7126 0 16.8781 0.0326726 17.0325 0.0961522C17.1869 0.159632 17.3272 0.252675 17.4454 0.36997C17.5636 0.487265 17.6573 0.626515 17.7213 0.779768C17.7853 0.933022 17.8182 1.09728 17.8182 1.26316V2.52632H24.1818C25.1945 2.52632 26.1656 2.92556 26.8817 3.63623C27.5977 4.34689 28 5.31076 28 6.31579V44.2105C28 45.2156 27.5977 46.1794 26.8817 46.8901C26.1656 47.6008 25.1945 48 24.1818 48H3.81818C2.80554 48 1.83437 47.6008 1.11832 46.8901C0.402272 46.1794 0 45.2156 0 44.2105V6.31579C0 5.31076 0.402272 4.34689 1.11832 3.63623C1.83437 2.92556 2.80554 2.52632 3.81818 2.52632H10.1818V1.26316C10.1818 0.928148 10.3159 0.606858 10.5546 0.36997C10.7933 0.133082 11.117 0 11.4545 0H16.5455ZM24.1818 5.05263H3.81818C3.48063 5.05263 3.15691 5.18571 2.91823 5.4226C2.67954 5.65949 2.54545 5.98078 2.54545 6.31579V44.2105C2.54545 44.5455 2.67954 44.8668 2.91823 45.1037C3.15691 45.3406 3.48063 45.4737 3.81818 45.4737H24.1818C24.5194 45.4737 24.8431 45.3406 25.0818 45.1037C25.3205 44.8668 25.4545 44.5455 25.4545 44.2105V6.31579C25.4545 5.98078 25.3205 5.65949 25.0818 5.4226C24.8431 5.18571 24.5194 5.05263 24.1818 5.05263Z" fill="url(#paint0_linear_1519_1955)"/> |
| | | <path d="M22.909 42.9474V8.33691H5.09082V42.9474H22.909Z" fill="#40FF5C"/> |
| | | </g> |
| | | <defs> |
| | | <linearGradient id="paint0_linear_1519_1955" x1="14" y1="0" x2="14" y2="48" gradientUnits="userSpaceOnUse"> |
| | | <stop offset="0.25" stop-color="#00FFF2"/> |
| | | <stop offset="0.514423" stop-color="#00FFF2"/> |
| | | <stop offset="0.721154" stop-color="#00FFF2"/> |
| | | </linearGradient> |
| | | </defs> |
| | | </svg> |
| New file |
| | |
| | | <svg width="28" height="48" viewBox="0 0 28 48" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| | | <g id="Group 1321316190"> |
| | | <path id="Vector" d="M16.3293 0C16.4942 0 16.6576 0.03249 16.81 0.0956148C16.9624 0.15874 17.1008 0.251263 17.2175 0.367902C17.3341 0.484542 17.4266 0.623013 17.4897 0.77541C17.5529 0.927807 17.5854 1.09114 17.5854 1.2561V2.5122H23.8659C24.8653 2.5122 25.8237 2.90921 26.5304 3.6159C27.2371 4.32259 27.6341 5.28107 27.6341 6.28049V43.9634C27.6341 44.9628 27.2371 45.9213 26.5304 46.628C25.8237 47.3347 24.8653 47.7317 23.8659 47.7317H3.76829C2.76888 47.7317 1.8104 47.3347 1.10371 46.628C0.397016 45.9213 0 44.9628 0 43.9634V6.28049C0 5.28107 0.397016 4.32259 1.10371 3.6159C1.8104 2.90921 2.76888 2.5122 3.76829 2.5122H10.0488V1.2561C10.0488 0.92296 10.1811 0.603466 10.4167 0.367902C10.6522 0.132338 10.9717 0 11.3049 0H16.3293ZM23.8659 5.02439H3.76829C3.43515 5.02439 3.11566 5.15673 2.8801 5.39229C2.64453 5.62786 2.5122 5.94735 2.5122 6.28049V43.9634C2.5122 44.2966 2.64453 44.616 2.8801 44.8516C3.11566 45.0872 3.43515 45.2195 3.76829 45.2195H23.8659C24.199 45.2195 24.5185 45.0872 24.754 44.8516C24.9896 44.616 25.122 44.2966 25.122 43.9634V6.28049C25.122 5.94735 24.9896 5.62786 24.754 5.39229C24.5185 5.15673 24.199 5.02439 23.8659 5.02439Z" fill="#00FFF2"/> |
| | | <path id="Vector_2" d="M22.6098 42.707V37.6826H5.02441V42.707H22.6098Z" fill="#F20303"/> |
| | | </g> |
| | | </svg> |
| New file |
| | |
| | | <svg width="28" height="48" viewBox="0 0 28 48" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| | | <g id="Vector"> |
| | | <path d="M16.5455 0C16.7126 0 16.8781 0.0326726 17.0325 0.0961522C17.1869 0.159632 17.3272 0.252675 17.4454 0.36997C17.5636 0.487265 17.6573 0.626515 17.7213 0.779768C17.7853 0.933022 17.8182 1.09728 17.8182 1.26316V2.52632H24.1818C25.1945 2.52632 26.1656 2.92556 26.8817 3.63623C27.5977 4.34689 28 5.31076 28 6.31579V44.2105C28 45.2156 27.5977 46.1794 26.8817 46.8901C26.1656 47.6008 25.1945 48 24.1818 48H3.81818C2.80554 48 1.83437 47.6008 1.11832 46.8901C0.402272 46.1794 0 45.2156 0 44.2105V6.31579C0 5.31076 0.402272 4.34689 1.11832 3.63623C1.83437 2.92556 2.80554 2.52632 3.81818 2.52632H10.1818V1.26316C10.1818 0.928148 10.3159 0.606858 10.5546 0.36997C10.7933 0.133082 11.117 0 11.4545 0H16.5455ZM24.1818 5.05263H3.81818C3.48063 5.05263 3.15691 5.18571 2.91823 5.4226C2.67954 5.65949 2.54545 5.98078 2.54545 6.31579V44.2105C2.54545 44.5455 2.67954 44.8668 2.91823 45.1037C3.15691 45.3406 3.48063 45.4737 3.81818 45.4737H24.1818C24.5194 45.4737 24.8431 45.3406 25.0818 45.1037C25.3205 44.8668 25.4545 44.5455 25.4545 44.2105V6.31579C25.4545 5.98078 25.3205 5.65949 25.0818 5.4226C24.8431 5.18571 24.5194 5.05263 24.1818 5.05263Z" fill="#00FFF2"/> |
| | | <path d="M22.9091 42.9474V23.4947H5.09091V42.9474H22.9091Z" fill="#00EEFF"/> |
| | | </g> |
| | | </svg> |
| New file |
| | |
| | | <template> |
| | | <div class="ztzf-cesium map-container" :id="props.containerId"></div> |
| | | <div v-if="props.showLayerControl" class="layer-control-root" :class="{ collapsed: props.leftCollapsed }"> |
| | | <div class="layer-control-wrap" ref="layerWrapRef"> |
| | | <div class="layer-control" @click="toggleLayerPanel"> |
| | | <img :src="layerControlIcon" alt="图层控制" /> |
| | | </div> |
| | | <div v-if="showLayerPanel" class="layer-panel"> |
| | | <div class="panel-title">图层管理</div> |
| | | |
| | | <div class="panel-content"> |
| | | <el-tree |
| | | :data="layerTree" |
| | | show-checkbox |
| | | default-expand-all |
| | | node-key="key" |
| | | :props="layerTreeProps" |
| | | :default-checked-keys="defaultCheckedKeys" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import * as Cesium from 'cesium' |
| | | import { PublicCesium } from '@/utils/cesium/publicCesium' |
| | | import layerControlIcon from '@/assets/images/dataCockpit/layerControl.png' |
| | | import equipmentIcon from '@/assets/images/dataCockpit/map/equipment.png' |
| | | |
| | | const props = defineProps({ |
| | | onlineDevices: { |
| | | type: Array, |
| | | default: () => [], |
| | | }, |
| | | leftCollapsed: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | containerId: { |
| | | type: String, |
| | | default: 'device-map-container', |
| | | }, |
| | | showLayerControl: { |
| | | type: Boolean, |
| | | default: true, |
| | | }, |
| | | }) |
| | | |
| | | let viewInstance = null |
| | | let viewer = null |
| | | const deviceEntityIds = new Set() |
| | | const showLayerPanel = ref(false) |
| | | const layerWrapRef = ref(null) |
| | | const layerTreeProps = { |
| | | label: 'label', |
| | | children: 'children', |
| | | } |
| | | const defaultCheckedKeys = ['global', 'city-base'] |
| | | const layerTree = ref([ |
| | | { |
| | | key: 'base', |
| | | label: '地理信息图层', |
| | | children: [ |
| | | { key: 'global', label: '全球地形' }, |
| | | { key: 'admin', label: '行政区划' }, |
| | | ], |
| | | }, |
| | | { |
| | | key: 'city', |
| | | label: '城市CIM图层', |
| | | children: [ |
| | | { key: 'city-base', label: '皖山白模' }, |
| | | { key: 'city-grid', label: '皖山白模光栅网格' }, |
| | | { key: 'city-tilt', label: '皖山倾斜摄影' }, |
| | | { key: 'city-tilt-grid', label: '皖山倾斜摄影网格' }, |
| | | ], |
| | | }, |
| | | { |
| | | key: 'sky', |
| | | label: '空域要素图层', |
| | | children: [ |
| | | { key: 'airspace', label: '空域边界' }, |
| | | { key: 'route', label: '飞行航路' }, |
| | | ], |
| | | }, |
| | | ]) |
| | | |
| | | const getDevicePosition = item => { |
| | | const longitudeRaw = item.longitude ?? item.lng ?? item.lon |
| | | const latitudeRaw = item.latitude ?? item.lat |
| | | if (longitudeRaw == null || latitudeRaw == null) return null |
| | | const longitude = Number(longitudeRaw) |
| | | const latitude = Number(latitudeRaw) |
| | | if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) return null |
| | | return { longitude, latitude } |
| | | } |
| | | |
| | | const clearDeviceEntities = () => { |
| | | if (!viewer) return |
| | | deviceEntityIds.forEach(id => { |
| | | viewer.entities.removeById(id) |
| | | }) |
| | | deviceEntityIds.clear() |
| | | } |
| | | |
| | | const RING_STYLES = [ |
| | | { inner: 0, outer: 5000, gradient: ['#FF361C', '#360B00'] }, |
| | | { inner: 5000, outer: 8000, gradient: ['#FFC609', '#583300'] }, |
| | | { inner: 8000, outer: 10000, gradient: ['#2AEDBF', '#012B11'] }, |
| | | ] |
| | | |
| | | const MATERIAL_TYPE = 'RadialGradientMaterial' |
| | | let materialRegistered = false |
| | | |
| | | const registerRadialGradientMaterial = () => { |
| | | if (materialRegistered || !Cesium?.Material) return |
| | | materialRegistered = true |
| | | Cesium.Material._materialCache.addMaterial(MATERIAL_TYPE, { |
| | | fabric: { |
| | | type: MATERIAL_TYPE, |
| | | uniforms: { |
| | | color1: new Cesium.Color(1.0, 1.0, 1.0, 1.0), |
| | | color2: new Cesium.Color(0.0, 0.0, 0.0, 1.0), |
| | | }, |
| | | source: ` |
| | | czm_material czm_getMaterial(czm_materialInput materialInput) { |
| | | czm_material material = czm_getDefaultMaterial(materialInput); |
| | | vec2 st = materialInput.st - vec2(0.5); |
| | | float t = clamp(length(st) * 2.0, 0.0, 1.0); |
| | | vec4 color = mix(color1, color2, t); |
| | | material.diffuse = color.rgb; |
| | | material.alpha = color.a; |
| | | return material; |
| | | } |
| | | `, |
| | | }, |
| | | translucent: () => true, |
| | | }) |
| | | } |
| | | |
| | | const buildCirclePositions = (center, radiusMeters, steps = 64) => { |
| | | const positions = [] |
| | | const latRad = Cesium.Math.toRadians(center.latitude) |
| | | const metersToLat = 1 / 111320 |
| | | const metersToLon = 1 / (111320 * Math.cos(latRad)) |
| | | for (let i = 0; i <= steps; i += 1) { |
| | | const angle = Cesium.Math.toRadians((i / steps) * 360) |
| | | const dx = radiusMeters * Math.cos(angle) |
| | | const dy = radiusMeters * Math.sin(angle) |
| | | const lon = center.longitude + dx * metersToLon |
| | | const lat = center.latitude + dy * metersToLat |
| | | positions.push(Cesium.Cartesian3.fromDegrees(lon, lat)) |
| | | } |
| | | return positions |
| | | } |
| | | |
| | | const addDeviceRings = (center, baseId) => { |
| | | RING_STYLES.forEach((ring, index) => { |
| | | const outerPositions = buildCirclePositions(center, ring.outer) |
| | | const holes = ring.inner |
| | | ? [new Cesium.PolygonHierarchy(buildCirclePositions(center, ring.inner))] |
| | | : [] |
| | | const entityId = `${baseId}-ring-${index}` |
| | | deviceEntityIds.add(entityId) |
| | | registerRadialGradientMaterial() |
| | | const [startColor, endColor] = ring.gradient |
| | | const color1 = Cesium.Color.fromCssColorString(startColor).withAlpha(0.34) |
| | | const color2 = Cesium.Color.fromCssColorString(endColor).withAlpha(0.34) |
| | | const material = new RadialGradientMaterialProperty(color1, color2) |
| | | viewer.entities.add({ |
| | | id: entityId, |
| | | polygon: { |
| | | hierarchy: new Cesium.PolygonHierarchy(outerPositions, holes), |
| | | material, |
| | | }, |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | class RadialGradientMaterialProperty { |
| | | constructor(color1, color2) { |
| | | this._definitionChanged = new Cesium.Event() |
| | | this.color1 = color1 |
| | | this.color2 = color2 |
| | | } |
| | | |
| | | get isConstant() { |
| | | return true |
| | | } |
| | | |
| | | get definitionChanged() { |
| | | return this._definitionChanged |
| | | } |
| | | |
| | | getType() { |
| | | return MATERIAL_TYPE |
| | | } |
| | | |
| | | getValue(time, result) { |
| | | const target = result || {} |
| | | target.color1 = this.color1 |
| | | target.color2 = this.color2 |
| | | return target |
| | | } |
| | | |
| | | equals(other) { |
| | | return ( |
| | | other instanceof RadialGradientMaterialProperty && |
| | | Cesium.Color.equals(this.color1, other.color1) && |
| | | Cesium.Color.equals(this.color2, other.color2) |
| | | ) |
| | | } |
| | | } |
| | | |
| | | const renderDeviceEntities = devices => { |
| | | if (!viewer) return |
| | | clearDeviceEntities() |
| | | devices.forEach((item, index) => { |
| | | const position = getDevicePosition(item) |
| | | if (!position) return |
| | | const entityId = `online-device-${item.id ?? index}` |
| | | deviceEntityIds.add(entityId) |
| | | addDeviceRings(position, entityId) |
| | | viewer.entities.add({ |
| | | id: entityId, |
| | | position: Cesium.Cartesian3.fromDegrees(position.longitude, position.latitude, 0), |
| | | billboard: { |
| | | image: equipmentIcon, |
| | | width: 40.34, |
| | | height: 40.34, |
| | | }, |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | watch( |
| | | () => props.onlineDevices, |
| | | devices => { |
| | | renderDeviceEntities(devices || []) |
| | | }, |
| | | { deep: true } |
| | | ) |
| | | |
| | | watch( |
| | | () => props.leftCollapsed, |
| | | isCollapsed => { |
| | | if (isCollapsed) showLayerPanel.value = false |
| | | } |
| | | ) |
| | | |
| | | const toggleLayerPanel = () => { |
| | | if (!props.showLayerControl) return |
| | | showLayerPanel.value = !showLayerPanel.value |
| | | } |
| | | |
| | | const handleClickOutside = event => { |
| | | if (!showLayerPanel.value) return |
| | | const target = event.target |
| | | if (layerWrapRef.value?.contains(target)) return |
| | | showLayerPanel.value = false |
| | | } |
| | | |
| | | onMounted(() => { |
| | | document.addEventListener('click', handleClickOutside) |
| | | viewInstance = new PublicCesium({ |
| | | dom: props.containerId, |
| | | flatMode: false, |
| | | terrain: false, |
| | | layerMode: 4, |
| | | contour: false, |
| | | }) |
| | | |
| | | viewer = viewInstance.getViewer() |
| | | renderDeviceEntities(props.onlineDevices) |
| | | }) |
| | | |
| | | onBeforeUnmount(() => { |
| | | document.removeEventListener('click', handleClickOutside) |
| | | clearDeviceEntities() |
| | | if (viewInstance) { |
| | | viewInstance?.viewerDestroy() |
| | | viewInstance = null |
| | | } |
| | | |
| | | viewer = null |
| | | }) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .map-container { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | |
| | | .layer-control-root { |
| | | position: absolute; |
| | | left: 411px; |
| | | bottom: 22px; |
| | | z-index: 9; |
| | | transition: transform 0.3s ease-in-out; |
| | | pointer-events: none; |
| | | |
| | | &.collapsed { |
| | | transform: translateX(-401px); |
| | | } |
| | | } |
| | | |
| | | .layer-control-wrap { |
| | | position: relative; |
| | | display: flex; |
| | | align-items: flex-end; |
| | | pointer-events: auto; |
| | | } |
| | | |
| | | .layer-control { |
| | | width: 34px; |
| | | height: 34px; |
| | | cursor: pointer; |
| | | |
| | | img { |
| | | width: 100%; |
| | | height: 100%; |
| | | display: block; |
| | | } |
| | | } |
| | | |
| | | .layer-panel { |
| | | display: flex; |
| | | flex-direction: column; |
| | | position: absolute; |
| | | left: 46px; |
| | | bottom: 0; |
| | | width: 160px; |
| | | max-height: 442px; |
| | | background: #191932; |
| | | border-radius: 8px 8px 8px 8px; |
| | | |
| | | .panel-title { |
| | | padding: 0 16px; |
| | | line-height: 42px; |
| | | font-family: 'Open Sans', Open Sans; |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: #ffffff; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | border-bottom: 1px solid rgba(70, 70, 100, 0.5); |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .panel-content { |
| | | padding: 0 16px; |
| | | height: 0; |
| | | flex: 1; |
| | | overflow: auto; |
| | | } |
| | | |
| | | ::v-deep(.el-tree) { |
| | | background: transparent; |
| | | color: #c3c3dd; |
| | | font-size: 12px; |
| | | |
| | | .el-tree-node { |
| | | line-height: 30px !important; |
| | | |
| | | .el-tree-node__content { |
| | | display: flex; |
| | | align-items: center; |
| | | height: 40px !important; |
| | | line-height: 40px !important; |
| | | border-bottom: 1px solid rgba(70, 70, 100, 0.5); |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .el-tree-node__children { |
| | | .el-tree-node__content { |
| | | border: none; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .layer-panel :deep(.el-tree-node__label) { |
| | | color: #c3c3dd; |
| | | } |
| | | |
| | | .layer-panel :deep(.el-tree-node__expand-icon) { |
| | | order: 3; |
| | | margin-left: auto; |
| | | } |
| | | |
| | | .layer-panel :deep(.el-tree-node__expand-icon.is-leaf) { |
| | | visibility: hidden; |
| | | } |
| | | |
| | | .layer-panel :deep(.el-checkbox) { |
| | | order: 1; |
| | | } |
| | | |
| | | .layer-panel :deep(.el-tree-node__label) { |
| | | order: 2; |
| | | } |
| | | |
| | | .layer-panel :deep(.el-tree-node__expand-icon:not(.is-leaf) ~ .el-checkbox) { |
| | | display: none; |
| | | } |
| | | |
| | | .layer-panel :deep(.el-tree-node__content:hover) { |
| | | background: transparent !important; |
| | | } |
| | | |
| | | .layer-panel :deep(.el-tree-node.is-current > .el-tree-node__content) { |
| | | background: transparent !important; |
| | | color: #ffffff; |
| | | } |
| | | |
| | | .layer-panel :deep(.el-tree-node.is-current > .el-tree-node__content .el-tree-node__label) { |
| | | color: #ffffff; |
| | | } |
| | | |
| | | .layer-panel :deep(.el-checkbox__input.is-checked .el-checkbox__inner) { |
| | | background-color: #023aff; |
| | | border-color: #023aff; |
| | | } |
| | | |
| | | .layer-panel :deep(.el-checkbox__inner) { |
| | | background-color: transparent; |
| | | border-color: #a1a3d4; |
| | | } |
| | | </style> |
| | |
| | | * @Date: 2024-10-25 15:07:51 |
| | | * @LastEditors: shuishen 1109946754@qq.com |
| | | * @LastEditTime: 2025-04-28 11:34:40 |
| | | * @FilePath: \drone-web-manage\src\components\map-container\mapContainer.vue |
| | | * @Description: |
| | | * |
| | | * Copyright (c) 2024 by shuishen, All Rights Reserved. |
| | | * @FilePath: \drone-command\src\components\map-container\mapContainer.vue |
| | | * @Description: |
| | | * |
| | | * Copyright (c) 2024 by shuishen, All Rights Reserved. |
| | | --> |
| | | <template> |
| | | <div class="map-container"> |
| | |
| | | height: 120%; |
| | | transform: translate(-50%, -50%); |
| | | } |
| | | </style> |
| | | </style> |
| | |
| | | * @Date: 2024-10-25 15:07:51 |
| | | * @LastEditors: shuishen 1109946754@qq.com |
| | | * @LastEditTime: 2025-04-28 11:34:40 |
| | | * @FilePath: \drone-web-manage\src\components\map-container\mapContainer.vue |
| | | * @FilePath: \drone-command\src\components\map-container\mapContainer.vue |
| | | * @Description: |
| | | * |
| | | * Copyright (c) 2024 by shuishen, All Rights Reserved. |
| | |
| | | /* |
| | | * @Author : yuan |
| | | * @Date : 2026-01-06 09:47:27 |
| | | * @LastEditors : yuan |
| | | * @LastEditTime : 2026-01-16 11:12:30 |
| | | * @FilePath : \applications\task-work-order\src\config\website.js |
| | | * @Description : |
| | | * Copyright 2026 OBKoro1, All Rights Reserved. |
| | | * 2026-01-06 09:47:27 |
| | | */ |
| | | /** |
| | | * 全局配置文件 |
| | | */ |
| | | export default { |
| | | title: 'saber', |
| | | logo: 'S', |
| | | key: 'saber', //配置主键,目前用于存储 |
| | | key: 'work', //配置主键,目前用于存储 |
| | | indexTitle: '', |
| | | clientId: 'drone', // 客户端id |
| | | clientSecret: 'drone_secret', // 客户端密钥 |
| | | clientId: 'saber', // 客户端id |
| | | clientSecret: 'saber_secret', // 客户端密钥 |
| | | tenantMode: true, // 是否开启租户模式 |
| | | tenantId: '000000', // 管理组租户编号 |
| | | captchaMode: true, // 是否开启验证码模式 |
| | |
| | | menu: true, |
| | | }, |
| | | fistPage: { |
| | | name: '个人工作台', |
| | | path: '/wel/index', |
| | | name: '数据驾驶舱', |
| | | path: '/dataCockpit/index', |
| | | // path: '/tickets/ticket', |
| | | |
| | | }, |
| | |
| | | i18n: 'dashboard', |
| | | }, |
| | | component: () => import(/* webpackChunkName: "views" */ '@/views/wel/index.vue'), |
| | | }, |
| | | { |
| | | path: 'dashboard', |
| | | name: '控制台', |
| | | meta: { |
| | | i18n: 'dashboard', |
| | | menu: false, |
| | | }, |
| | | component: () => import(/* webpackChunkName: "views" */ '@/views/wel/dashboard.vue'), |
| | | }, |
| | | } |
| | | ], |
| | | }, |
| | | // 事件工单 |
| | |
| | | }) |
| | | } |
| | | return new Promise(resolve => { |
| | | getRoutes({tenantId,sysType: 1}).then(res => { |
| | | getRoutes({tenantId,sysType: 5}).then(res => { |
| | | const data = res.data.data; |
| | | let menu = deepClone(data); |
| | | if (!res.data?.data?.length){ |
| | |
| | | }, |
| | | GetButtons({ commit }) { |
| | | return new Promise(resolve => { |
| | | getButtons({sysType: 1}).then(res => { |
| | | getButtons({sysType: 5}).then(res => { |
| | | const data = res.data.data; |
| | | commit('SET_PERMISSION', data); |
| | | resolve(); |
| File was renamed from applications/task-work-order/src/views/layerManagement/components/utils.js |
| | |
| | | import * as Cesium from 'cesium' |
| | | import _, { cloneDeep, throttle } from 'lodash' |
| | | import * as turf from '@turf/turf' |
| | | import { boxTransformScale } from '@/utils/turfFunc' |
| | | import { ElMessage } from 'element-plus' |
| | | import { flyVisual, getPointPositionsHeight } from '@/utils/cesium/mapUtil' |
| | | |
| | | /** |
| | | * 多边形绘制与编辑工具类 |
| | |
| | | */ |
| | | export class DrawPolygon { |
| | | constructor() { |
| | | // 是否绘制 |
| | | this.whetherToDraw =false |
| | | // 图斑预览模式标记 |
| | | this.isPureSpotPreview = false |
| | | // 是否删除测区 |
| | |
| | | // 存储端点的 DataSource |
| | | this.editPolygonDataSource = null |
| | | this.editPolygonPointDataSource = null |
| | | // 存储中点(边中点)的 DataSource:用于编辑态插入新端点 |
| | | this.editPolygonMidPointDataSource = null |
| | | // 鼠标事件处理器 |
| | | this.handler = null |
| | | // 右键菜单 DOM |
| | |
| | | this.delPoint = this.delPoint.bind(this) |
| | | // 外部订阅者 |
| | | this.listeners = [] |
| | | |
| | | } |
| | | |
| | | // 实体命名常量 |
| | | static ENTITY_NAMES = { |
| | | POLYGON: '区域-面', |
| | | POINT: '区域-端点', |
| | | // 编辑态中点(用于快速插入新端点) |
| | | MID_POINT: '区域-中点', |
| | | POLYGON_ID: 'planar-route-polygon', |
| | | POINT_ID_PREFIX: 'planar-route-point', |
| | | MID_POINT_ID_PREFIX: 'planar-route-mid-point', |
| | | } |
| | | |
| | | // 颜色常量 |
| | |
| | | // 编辑图斑 |
| | | editThePatch(data) { |
| | | this.isPreviewMode = data |
| | | } |
| | | // 绘制 |
| | | drawTheArea(data){ |
| | | this.whetherToDraw = data |
| | | // 关闭编辑能力时隐藏端点/中点,并清空中点,避免残留交互 |
| | | if (!this.isPreviewMode) { |
| | | this.editPolygonPointDataSource && (this.editPolygonPointDataSource.entities.show = false) |
| | | this.editPolygonMidPointDataSource && (this.editPolygonMidPointDataSource.entities.show = false) |
| | | this.editPolygonMidPointDataSource?.entities?.removeAll?.() |
| | | return |
| | | } |
| | | |
| | | if (this.editingMode) { |
| | | this.editPolygonPointDataSource && (this.editPolygonPointDataSource.entities.show = true) |
| | | this.editPolygonMidPointDataSource && (this.editPolygonMidPointDataSource.entities.show = true) |
| | | this.rebuildEditPoints() |
| | | this.rebuildMidPoints() |
| | | } |
| | | } |
| | | // 删除测区 |
| | | deleteTheArea(data) { |
| | |
| | | |
| | | // 开始绘制 |
| | | startDrawing() { |
| | | if (!this.whetherToDraw) { |
| | | |
| | | return; |
| | | } |
| | | |
| | | |
| | | this.drawingMode = true |
| | | this.curPolygon = new Cesium.PolygonHierarchy() |
| | | |
| | |
| | | this.viewer?.dataSources.add(this.editPolygonPointDataSource) |
| | | } |
| | | |
| | | // 中点数据源:编辑模式下用于“边上插点” |
| | | if (!this.editPolygonMidPointDataSource) { |
| | | this.editPolygonMidPointDataSource = new Cesium.CustomDataSource('editPolygonMidPointDataSource') |
| | | this.viewer?.dataSources.add(this.editPolygonMidPointDataSource) |
| | | } |
| | | |
| | | // 清空之前的点 |
| | | this.editPolygonDataSource?.entities.removeAll() |
| | | this.editPolygonPointDataSource?.entities.removeAll() |
| | | this.editPolygonMidPointDataSource?.entities.removeAll() |
| | | } |
| | | |
| | | // 创建多边形实体(含边界线) |
| | |
| | | ind: pointIndex, // 索引记录 |
| | | }, |
| | | }) |
| | | |
| | | |
| | | } |
| | | |
| | | createMidPointEntity(startInd, position) { |
| | | // 使用 CallbackProperty 让中点位置随端点拖拽实时计算 |
| | | // startInd 表示该中点属于边:(startInd) -> (startInd + 1) |
| | | const updatePosition = () => { |
| | | const positions = this.curPolygon?.positions |
| | | if (!positions || positions.length < 2) return position |
| | | |
| | | const n = positions.length |
| | | const p1 = positions[startInd] |
| | | const p2 = positions[(startInd + 1) % n] |
| | | if (!p1 || !p2) return position |
| | | |
| | | return Cesium.Cartesian3.midpoint(p1, p2, new Cesium.Cartesian3()) |
| | | } |
| | | |
| | | this.editPolygonMidPointDataSource.entities.add({ |
| | | name: DrawPolygon.ENTITY_NAMES.MID_POINT, |
| | | id: `${DrawPolygon.ENTITY_NAMES.MID_POINT_ID_PREFIX}${startInd}`, |
| | | position: new Cesium.CallbackProperty(updatePosition, false), |
| | | point: { |
| | | pixelSize: 10, |
| | | color: Cesium.Color.fromCssColorString('rgba(255, 255, 255, .7)'), |
| | | heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | }, |
| | | customData: { |
| | | startInd, |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | rebuildEditPoints() { |
| | | // 插入/删除端点后:端点实体 id 与 customData.ind 需要全量重建,保证索引与 positions 一致 |
| | | if (!this.isPreviewMode) return |
| | | if (!this.editPolygonPointDataSource || !this.curPolygon?.positions) return |
| | | |
| | | this.editPolygonPointDataSource.entities.removeAll() |
| | | this.curPolygon.positions.forEach((position, index) => { |
| | | this.editPolygonPointDataSource.entities.add({ |
| | | name: DrawPolygon.ENTITY_NAMES.POINT, |
| | | id: `${DrawPolygon.ENTITY_NAMES.POINT_ID_PREFIX}${index}`, |
| | | position: position.clone(), |
| | | point: { |
| | | pixelSize: 14, |
| | | color: Cesium.Color.WHITE, |
| | | heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | }, |
| | | customData: { |
| | | ind: index, |
| | | }, |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | rebuildMidPoints() { |
| | | // 仅在“顶点数量变化/切换编辑态”时维护中点实体数量 |
| | | // 拖拽过程中中点位置由 CallbackProperty 自动更新,不需要重建 |
| | | if (!this.isPreviewMode) return |
| | | if (!this.editPolygonMidPointDataSource || !this.curPolygon?.positions) return |
| | | |
| | | const positions = this.curPolygon.positions |
| | | const n = positions.length |
| | | const entities = this.editPolygonMidPointDataSource.entities |
| | | |
| | | if (!this.editingMode || n < 2) { |
| | | entities.removeAll() |
| | | return |
| | | } |
| | | |
| | | const neededIds = new Set() |
| | | for (let i = 0; i < n; i += 1) { |
| | | const id = `${DrawPolygon.ENTITY_NAMES.MID_POINT_ID_PREFIX}${i}` |
| | | neededIds.add(id) |
| | | // 缺少则补齐:保证每条边都有一个中点实体 |
| | | if (!entities.getById(id)) { |
| | | const p1 = positions[i] |
| | | const p2 = positions[(i + 1) % n] |
| | | if (!p1 || !p2) continue |
| | | const mid = Cesium.Cartesian3.midpoint(p1, p2, new Cesium.Cartesian3()) |
| | | this.createMidPointEntity(i, mid) |
| | | } |
| | | } |
| | | |
| | | entities.values.slice().forEach(entity => { |
| | | if (entity?.name !== DrawPolygon.ENTITY_NAMES.MID_POINT) return |
| | | // 多余则移除:例如删除端点导致边数量减少 |
| | | if (!neededIds.has(entity.id)) { |
| | | entities.remove(entity) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | insertPointFromMidPoint(midPointEntity) { |
| | | // 点击中点:在对应边上插入一个新端点(白点),并立即进入拖拽态 |
| | | if (!this.isPreviewMode) return |
| | | if (!this.editingMode) return |
| | | if (!midPointEntity?.customData) return |
| | | |
| | | const startInd = midPointEntity.customData.startInd |
| | | // 中点位置可能是 CallbackProperty,需要取当前时刻的值 |
| | | const positionProperty = midPointEntity.position |
| | | const entityPosition = positionProperty?.getValue |
| | | ? positionProperty.getValue(Cesium.JulianDate.now()) |
| | | : positionProperty |
| | | if (!entityPosition) return |
| | | |
| | | // 插入到 startInd 与 startInd+1 之间 |
| | | const insertIndex = Math.min(Math.max(startInd + 1, 0), this.curPolygon.positions.length) |
| | | this.curPolygon.positions.splice(insertIndex, 0, entityPosition.clone ? entityPosition.clone() : entityPosition) |
| | | |
| | | this.rebuildEditPoints() |
| | | this.rebuildMidPoints() |
| | | |
| | | const newPointEntity = this.editPolygonPointDataSource?.entities?.getById( |
| | | `${DrawPolygon.ENTITY_NAMES.POINT_ID_PREFIX}${insertIndex}` |
| | | ) |
| | | if (newPointEntity) { |
| | | this.isDragging = true |
| | | this.draggedEntity = newPointEntity |
| | | this.currentDragPointPosition = this.curPolygon.positions[insertIndex] |
| | | this.disableMapControl() |
| | | } |
| | | this.notify('getPolygonPositions', this.curPolygon.positions) |
| | | } |
| | | // 清除不在范围内的点 |
| | | removeLastInvalidPoint() { |
| | | const posLen = this.curPolygon.positions.length |
| | | if (posLen === 0) return |
| | | |
| | | let targetPointId = '' |
| | | let removeCount = 0 |
| | | |
| | | if (posLen === 2) { |
| | | removeCount = 2 |
| | | targetPointId = `${DrawPolygon.ENTITY_NAMES.POINT_ID_PREFIX}0` |
| | | } else if (posLen > 2) { |
| | | removeCount = 1 |
| | | const pointIndex = posLen - 2 |
| | | targetPointId = `${DrawPolygon.ENTITY_NAMES.POINT_ID_PREFIX}${pointIndex}` |
| | | } |
| | | |
| | | this.curPolygon.positions.splice(posLen - removeCount, removeCount) |
| | | |
| | | const invalidPoint = this.editPolygonPointDataSource.entities.getById(targetPointId) |
| | | if (invalidPoint) { |
| | | this.editPolygonPointDataSource.entities.remove(invalidPoint) |
| | | } |
| | | |
| | | if (this.curPolygon.positions.length === 0 && this.polygonEntity) { |
| | | this.editPolygonDataSource.entities.remove(this.polygonEntity) |
| | | this.polygonEntity = null |
| | | } |
| | | } |
| | | // 添加一个点 |
| | | addPosition(position, isAdd = true) { |
| | | // 第一个点要重复压入一次,形成动态绘制效果 |
| | |
| | | |
| | | const pickedEntity = this.viewer.scene.pick(movement.position)?.id |
| | | const isPoint = pickedEntity?.name === DrawPolygon.ENTITY_NAMES.POINT |
| | | const isMidPoint = pickedEntity?.name === DrawPolygon.ENTITY_NAMES.MID_POINT |
| | | |
| | | if (pickedEntity && isPoint) { |
| | | this.isDragging = true |
| | | this.draggedEntity = pickedEntity |
| | | this.currentDragPointPosition = this.curPolygon.positions[this.draggedEntity?.customData.ind] |
| | | this.disableMapControl() // 禁止地图交互 |
| | | return |
| | | } |
| | | |
| | | if (pickedEntity && isMidPoint) { |
| | | this.insertPointFromMidPoint(pickedEntity) |
| | | } |
| | | } |
| | | |
| | |
| | | this.isDragging = false |
| | | this.draggedEntity = null |
| | | this.enableMapControl() |
| | | this.rebuildMidPoints() |
| | | } |
| | | } |
| | | |
| | |
| | | // 编辑模式下,拖拽点实时更新 |
| | | if (this.editingMode && this.draggedEntity?.customData) { |
| | | this.draggedEntity.position = cartesian |
| | | this.curPolygon.positions[this.draggedEntity?.customData.ind] = cartesian |
| | | this.curPolygon.positions[this.draggedEntity?.customData.ind] = cartesian |
| | | } |
| | | |
| | | // 绘制模式下,实时更新最后一个点 |
| | |
| | | // 鼠标左键点击 |
| | | handleLeftClick(click) { |
| | | this.removeMenuPopup() |
| | | |
| | | const pickedAllEntity = this.viewer.scene.drillPick(click.position).filter(i => i.id) |
| | | const isStartPoint = pickedAllEntity.find(i => i.id.name === '起飞点') |
| | | const isPolygonPoint = pickedAllEntity.find(i => i.id.name === DrawPolygon.ENTITY_NAMES.POINT) |
| | | const isPolygon = pickedAllEntity.find(i => i.id.name === DrawPolygon.ENTITY_NAMES.POLYGON) |
| | | if (this.drawingMode && !this.whetherToDraw) { |
| | | |
| | | return; // 阻断后续绘制逻辑 |
| | | } |
| | | |
| | | if (isStartPoint) return |
| | | |
| | | // 如果不是绘制模式 |
| | | if (!this.drawingMode) { |
| | | if (!isPolygon) { |
| | | |
| | | |
| | | this.editingMode = false |
| | | this.editPolygonPointDataSource.entities.show = false |
| | | this.editPolygonMidPointDataSource && (this.editPolygonMidPointDataSource.entities.show = false) |
| | | return |
| | | } |
| | | |
| | | if (isPolygon) { |
| | | |
| | | this.editingMode = true |
| | | this.editPolygonPointDataSource.entities.show = true |
| | | this.editPolygonMidPointDataSource && (this.editPolygonMidPointDataSource.entities.show = true) |
| | | this.rebuildMidPoints() |
| | | } |
| | | return |
| | | } |
| | |
| | | const cartesian = this.viewer.scene.pickPosition(click.position) |
| | | if (!cartesian) return |
| | | |
| | | let arr = [...this.curPolygon.positions] |
| | | arr.pop() |
| | | // 校验多边形是否自交 |
| | | if (arr.length > 2 && !this.curDragPointIsValid([...arr, cartesian])) { |
| | | ElMessage.warning('测区不支持交叉绘制,请重新绘制') |
| | | return |
| | | } |
| | | |
| | | this.addPosition(cartesian) |
| | | } |
| | | |
| | | // 鼠标右键点击(弹出菜单) |
| | | handleRightClick(click) { |
| | | handleRightClick(click) { |
| | | const that = this |
| | | if (that.drawingMode) return |
| | | |
| | | that.removeMenuPopup() |
| | | |
| | | const pickedAllEntity = that.viewer.scene.drillPick(click.position).filter(i => i.id) |
| | |
| | | } = click |
| | | |
| | | let pickedEntity, tooltipEvent, menuType |
| | | if (isPolygon) { |
| | | pickedEntity = isPolygon |
| | | tooltipEvent = that.delPolygon |
| | | menuType = 'polygon' |
| | | |
| | | } else if (isEditPoint) { |
| | | if (isEditPoint) { |
| | | pickedEntity = isEditPoint |
| | | tooltipEvent = that.delPoint |
| | | menuType = 'edit-point' |
| | | |
| | | } else if (isPolygon) { |
| | | pickedEntity = isPolygon |
| | | tooltipEvent = that.delPolygon |
| | | menuType = 'polygon' |
| | | } |
| | | |
| | | if (pickedEntity && this.isDeleteTheArea) { |
| | |
| | | this.editPolygonPointDataSource.entities.removeAll() |
| | | this.editPolygonPointDataSource = null |
| | | } |
| | | |
| | | if (this.editPolygonMidPointDataSource) { |
| | | this.editPolygonMidPointDataSource.entities.removeAll() |
| | | this.editPolygonMidPointDataSource = null |
| | | } |
| | | this.editingMode = false |
| | | this.polygonEntity = null |
| | | this.curPolygon = null |
| | |
| | | this.drawingMode = false |
| | | this.editingMode = true |
| | | this.editPolygonPointDataSource.entities.show = true |
| | | this.editPolygonMidPointDataSource && (this.editPolygonMidPointDataSource.entities.show = true) |
| | | this.rebuildMidPoints() |
| | | |
| | | if (!this.curDragPointIsValid(this.curPolygon.positions)) { |
| | | this.isShowWaringTip = true |
| | |
| | | this.notify('getPolygonPositions', []) |
| | | this.startDrawing() |
| | | } |
| | | |
| | | // 删除图斑 |
| | | delSpot() { |
| | | this.removeEntities() |
| | | this.isPreviewMode = true |
| | | this.startDrawing() |
| | | } |
| | | // 删除端点 |
| | | delPoint() { |
| | | if (this.curPolygon.positions.length <= 3) { |
| | |
| | | if (!this.delPolygonPoint) return |
| | | |
| | | this.curPolygon.positions.splice(this.delPolygonPoint.customData.ind, 1) |
| | | this.editPolygonPointDataSource.entities.remove(this.delPolygonPoint) |
| | | this.removeMenuPopup() |
| | | |
| | | // 更新剩余点索引 |
| | | this.editPolygonPointDataSource.entities.values.forEach((item, index) => { |
| | | item.customData.ind = index |
| | | }) |
| | | this.rebuildEditPoints() |
| | | this.rebuildMidPoints() |
| | | |
| | | this.notify('getPolygonPositions', this.curPolygon.positions) |
| | | } |
| | |
| | | menuPopup.id = 'planarPolygonEditMenu' |
| | | menuPopup.className = 'planar-polygon-edit-menu' |
| | | |
| | | |
| | | const menuItems = |
| | | type === 'polygon' |
| | | ? [{ title: '删除测区', class: 'del-planar-polygon' }] |
| | |
| | | titleDiv.className = item.class |
| | | menuPopup.appendChild(titleDiv) |
| | | }) |
| | | |
| | | this.isPreviewMode = true |
| | | menuPopupVBox.appendChild(menuPopup) |
| | | return menuPopupVBox |
| | |
| | | |
| | | return intersections.features.length === 0 |
| | | } |
| | | |
| | | // 初始化已有多边形 |
| | | initPolygon(viewer, positions, isPurePreview = false) { |
| | | async initPolygon(viewer, positions, isPurePreview = false) { |
| | | this.initHandler(viewer) |
| | | |
| | | if (this.whetherToDraw) { |
| | | this.startDrawing(); |
| | | } else { |
| | | // 新增:不允许绘制时,确保绘制模式为 false,避免误触发 |
| | | this.drawingMode = false; |
| | | |
| | | } |
| | | |
| | | this.isPureSpotPreview = isPurePreview |
| | | this.startDrawing() |
| | | let newPosition = positions.map(item => { |
| | | return Cesium.Cartesian3.fromDegrees(Number(item.lng), Number(item.lat), Number(item.height)) |
| | | return Cesium.Cartesian3.fromDegrees(Number(item.lng), Number(item.lat), Number(item?.height || 0)) |
| | | }) |
| | | |
| | | // 预览航线的时候调用 |
| | | if (!this.isPureSpotPreview) { |
| | | this.notify('getPolygonPositions', newPosition) |
| | | } |
| | | |
| | | newPosition.forEach(item => { |
| | | this.addPosition(item, false) |
| | |
| | | duration: 0.5, |
| | | }) |
| | | |
| | | let pointList = await getPointPositionsHeight(positions, viewer) |
| | | flyVisual({ positionsData: pointList.map(item => [item.lng, item.lat, item.ASL]), viewer }) |
| | | |
| | | this.drawingMode = false |
| | | this.editingMode = true |
| | | this.editPolygonPointDataSource && (this.editPolygonPointDataSource.entities.show = true) |
| | | this.editPolygonMidPointDataSource && (this.editPolygonMidPointDataSource.entities.show = true) |
| | | this.rebuildMidPoints() |
| | | } |
| | | |
| | | // 初始化事件处理器 |
| | | initHandler(viewer) { |
| | | this.viewer = viewer |
| | | |
| | | if(this.whetherToDraw){ |
| | | this.startDrawing() |
| | | } |
| | | this.startDrawing() |
| | | |
| | | if (!this.handler) { |
| | | this.handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas) |
| | |
| | | this._opacity = undefined |
| | | this._alphaPower = undefined |
| | | this._speed = undefined |
| | | this._xRepeatCount = undefined |
| | | |
| | | this.color = options.color || Cesium.Color.fromBytes(0, 255, 255, 255) |
| | | this.glowPower = .25 |
| | |
| | | this.opacity = options.opacity || 0.5 |
| | | this.alphaPower = options.alphaPower || 1.5 |
| | | this.speed = options.speed || 5 |
| | | this.xRepeatCount = options.xRepeatCount || 1 |
| | | |
| | | |
| | | } |
| | | |
| | | get isConstant () { |
| | |
| | | color: new Cesium.Color(1.0, 1.0, 0.0, 0.7), |
| | | bgColor: new Cesium.Color(0.0, 1.0, 0.0, 0.0), |
| | | speed: 5, |
| | | globalAlpha: 1.0 |
| | | globalAlpha: 1.0, |
| | | xRepeatCount: 1.0 |
| | | }, |
| | | source: ` |
| | | uniform vec4 bgColor; |
| | | uniform vec4 color; |
| | | uniform float speed; |
| | | uniform float globalAlpha; |
| | | uniform float xRepeatCount; // 新增的 uniform 在着色器中 |
| | | |
| | | czm_material czm_getMaterial(czm_materialInput materialInput) { |
| | | czm_material czm_getMaterial(czm_materialInput materialInput) { |
| | | czm_material material = czm_getDefaultMaterial(materialInput); |
| | | vec2 st = materialInput.st; |
| | | float time = fract(czm_frameNumber * speed / 1000.0); |
| | |
| | | colorMars3D = vec3(1.0); |
| | | } |
| | | |
| | | material.alpha = color.a * 1.5 * smoothstep(0.0, 1.0, fract(st.s - time)); |
| | | material.alpha = color.a * 1.5 * smoothstep(0.0, 1.0, fract(st.s * xRepeatCount - time)); // 使用 xRepeatCount 来调整纹理重复 |
| | | material.diffuse = max(colorMars3D.rgb * material.alpha, colorMars3D.rgb); |
| | | |
| | | if (material.alpha < bgColor.a) { |
| | |
| | | result.color = Cesium.Property.getValueOrUndefined(this._color, time) |
| | | result.globalAlpha = Cesium.Property.getValueOrUndefined(this._opacity, time) |
| | | result.speed = Cesium.Property.getValueOrUndefined(this._speed, time) |
| | | result.xRepeatCount = Cesium.Property.getValueOrUndefined(this._xRepeatCount, time) |
| | | |
| | | return result |
| | | } |
| | | |
| | |
| | | (other instanceof LineTrailMaterial && |
| | | Cesium.Property.equals(this._color, other._color) && |
| | | Cesium.Property.equals(this._opacity, other._opacity) && |
| | | Cesium.Property.equals(this._speed, other._speed)) |
| | | Cesium.Property.equals(this._speed, other._speed) && |
| | | Cesium.Property.equals(this._xRepeatCount, other._xRepeatCount)) |
| | | ) |
| | | } |
| | | } |
| | |
| | | color: Cesium.createPropertyDescriptor('color'), |
| | | opacity: Cesium.createPropertyDescriptor('opacity'), |
| | | speed: Cesium.createPropertyDescriptor('speed'), |
| | | xRepeatCount: Cesium.createPropertyDescriptor('xRepeatCount'), |
| | | }) |
| | | |
| | | |
| | |
| | | let newLat = lat |
| | | |
| | | switch (direction) { |
| | | case 'W': // 向前(heading 方向) |
| | | case 'KeyW': // 向前(heading 方向) |
| | | newLon += moveStep * Math.sin(headingRad) |
| | | newLat += moveStep * Math.cos(headingRad) |
| | | break |
| | | |
| | | case 'S': // 向后(反方向) |
| | | case 'KeyS': // 向后(反方向) |
| | | newLon -= moveStep * Math.sin(headingRad) |
| | | newLat -= moveStep * Math.cos(headingRad) |
| | | break |
| | | |
| | | case 'D': // 向右(heading + 90°) |
| | | case 'KeyD': // 向右(heading + 90°) |
| | | newLon += moveStep * Math.cos(headingRad) |
| | | newLat -= moveStep * Math.sin(headingRad) |
| | | break |
| | | |
| | | case 'A': // 向左(heading - 90°) |
| | | case 'KeyA': // 向左(heading - 90°) |
| | | newLon -= moveStep * Math.cos(headingRad) |
| | | newLat += moveStep * Math.sin(headingRad) |
| | | break |
| New file |
| | |
| | | /** |
| | | * @description: 图片压缩 |
| | | * @param {*} src 图片地址 |
| | | * @param {*} quality 压缩质量 |
| | | * @param {*} maxWidth 最大宽度 |
| | | * @param {*} callback 回调函数 |
| | | * @return {*} |
| | | */ |
| | | function compressImage(src, quality, maxWidth, callback) { |
| | | const img = new Image() |
| | | img.setAttribute('crossOrigin', 'Anonymous') |
| | | img.onload = function () { |
| | | const canvas = document.createElement('canvas') |
| | | const ctx = canvas.getContext('2d') |
| | | let width = img.width |
| | | let height = img.height |
| | | |
| | | if (width > maxWidth) { |
| | | height *= maxWidth / width |
| | | width = maxWidth |
| | | } |
| | | |
| | | canvas.width = width |
| | | canvas.height = height |
| | | ctx.drawImage(img, 0, 0, width, height) |
| | | |
| | | const newData = canvas.toDataURL('image/jpeg', quality) |
| | | // 使用atob()将base64转换为二进制字符串 |
| | | const binaryString = window.atob(newData.split(',')[1]) |
| | | const mimeString = newData.split(',')[0].split(':')[1].split(';')[0] |
| | | // 创建Blob对象 |
| | | const array = [] |
| | | for (let i = 0; i < binaryString.length; i++) { |
| | | array.push(binaryString.charCodeAt(i)) |
| | | } |
| | | const blob = new Blob([new Uint8Array(array)], { type: mimeString }) |
| | | |
| | | callback(blob) |
| | | } |
| | | img.src = src |
| | | |
| | | return src |
| | | } |
| | | |
| | | export default function imageCompress(src, quality, maxWidth) { |
| | | // 使用例子 |
| | | return compressImage(src, quality, maxWidth, function (blob) { |
| | | // 创建一个新的图片URL |
| | | const imageUrl = URL.createObjectURL(blob) |
| | | // 可以在这里使用imageUrl,比如设置为img元素的src |
| | | // document.getElementById('your-image-element').src = imageUrl; |
| | | // 当不需要这个URL的时候,释放它 |
| | | URL.revokeObjectURL(imageUrl) |
| | | }) |
| | | } |
| New file |
| | |
| | | /* |
| | | * @Author: GuLiMmo 2820890765@qq.com |
| | | * @Date: 2024-05-21 10:14:11 |
| | | * @LastEditors: GuLiMmo 2820890765@qq.com |
| | | * @LastEditTime: 2024-05-21 10:20:34 |
| | | * @FilePath: /bigScreen/src/utils/cesium/compressImage2.js |
| | | * @Description: |
| | | * Copyright (c) 2024 by GuLiMmo, All Rights Reserved. |
| | | */ |
| | | export default function compressImage( |
| | | src, |
| | | maxWidth, |
| | | maxHeight, |
| | | outputFormat = 'image/jpeg', |
| | | quality = 0.7 |
| | | ) { |
| | | return new Promise((resolve, reject) => { |
| | | const image = new Image() |
| | | image.setAttribute('crossOrigin', 'Anonymous') |
| | | image.src = src |
| | | image.onload = () => { |
| | | let width = image.width |
| | | let height = image.height |
| | | |
| | | if (width > maxWidth) { |
| | | height *= maxWidth / width |
| | | width = maxWidth |
| | | } |
| | | |
| | | if (height > maxHeight) { |
| | | width *= maxHeight / height |
| | | height = maxHeight |
| | | } |
| | | |
| | | const canvas = document.createElement('canvas') |
| | | canvas.width = width |
| | | canvas.height = height |
| | | const ctx = canvas.getContext('2d') |
| | | ctx.drawImage(image, 0, 0, width, height) |
| | | |
| | | const newDataUrl = canvas.toDataURL(outputFormat, quality) |
| | | resolve(newDataUrl) |
| | | } |
| | | image.onerror = reject |
| | | }) |
| | | } |
| | |
| | | import { Decimal } from 'decimal.js' |
| | | import _, { cloneDeep, throttle } from 'lodash' |
| | | import { v4 as uuidv4 } from 'uuid' |
| | | |
| | | import { render } from 'vue' |
| | | |
| | | import { analysisPointLineKmz, handlePointListForKmz } from '@/views/newRoutePlan/Waypoint/pointWayLineUtils' |
| | | import HistoryDronePopup from '@/components/DeviceJobDetails/HistoryDronePopup.vue' |
| | | |
| | | import { getNewPolygonData } from '@/views/RoutePlan/PlanarAirLine/planarRouteUtils' |
| | | import { |
| | | getNewPolygonData, |
| | | } from '@/views/newRoutePlan/routeUtils' |
| | | |
| | | import { analyzeKmzFile, removeTextKey, XMLToJSON } from '@/utils/cesium/kmz' |
| | | |
| | | import { ArrowLineMaterialProperty, LineTrailMaterial, PolylineGlowMaterial, } from '@/utils/cesium/Material' |
| | | |
| | | import { ArrowLineMaterialProperty, LineTrailMaterial, PolylineGlowMaterial } from '@/utils/cesium/Material' |
| | | import CreateFrustum from '@/utils/cesium/frustum/CreateFrustum' |
| | | import { ElMessage } from 'element-plus' |
| | | |
| | |
| | | import aircraftGltf from '@/assets/gltf/aircraft.gltf' |
| | | import newNumPoint from '@/assets/images/newStartPoint.png' |
| | | |
| | | import { |
| | | getPolyLine, |
| | | } from '@/views/RoutePlan/PointAirLine/pointWayLineUtils' |
| | | import { getPolyLine } from '@/views/newRoutePlan/Waypoint/pointWayLineUtils' |
| | | import store from '@/store/index' |
| | | import newStartPoint from '@/assets/images/newStartPoint.png' |
| | | import newStartPointMore from '@/assets/images/newStartPointMore.png' |
| | | import { |
| | | arrowLineMaterialProperty, arrowLineMaterialPropertyGray, arrowLineMaterialPropertyGrayT, |
| | | arrowLineMaterialPropertyGreen, |
| | | arrowLineMaterialPropertyOrange, |
| | | } from '@/hooks/useMorePointLine/useMorePointLine' |
| | | |
| | | let arrowLineMaterialProperty = new ArrowLineMaterialProperty({ |
| | | color: new Cesium.Color(128 / 255, 215 / 255, 255 / 255, 1), |
| | | directionColor: new Cesium.Color(1, 1, 1, 1), |
| | | outlineColor: new Cesium.Color(1, 1, 1, 1), |
| | | outlineWidth: 0, |
| | | speed: 5 |
| | | }) |
| | | |
| | | let arrowLineMaterialPropertyOrange = new ArrowLineMaterialProperty({ |
| | | color: new Cesium.Color(255 / 255, 185 / 255, 58 / 255, 1), |
| | | directionColor: new Cesium.Color(1, 1, 1, 1), |
| | | outlineColor: new Cesium.Color(1, 1, 1, 1), |
| | | outlineWidth: 0, |
| | | speed: 5, |
| | | }) |
| | | |
| | | let arrowLineMaterialPropertyGreen = new ArrowLineMaterialProperty({ |
| | | color: new Cesium.Color(6 / 255, 217 / 255, 87 / 255, 1), |
| | | directionColor: new Cesium.Color(1, 1, 1, 1), |
| | | outlineColor: new Cesium.Color(1, 1, 1, 1), |
| | | outlineWidth: 0, |
| | | speed: 5, |
| | | }) |
| | | |
| | | // 记录index |
| | | let indexLog = 0 |
| | | // 颜色 |
| | | function getLineColor(lineNumber) { |
| | | // 颜色顺序: 蓝(0), 黄(1), 绿(2) |
| | | const colors = [arrowLineMaterialProperty, arrowLineMaterialPropertyOrange, arrowLineMaterialPropertyGreen] |
| | | // 计算颜色索引 (从0开始) |
| | | const colorIndex = (lineNumber - 1) % 3 |
| | | // 返回对应颜色 |
| | | return colors[colorIndex] |
| | | } |
| | | |
| | | let runningLineMaterial = new LineTrailMaterial({ |
| | | color: Cesium.Color.fromCssColorString('#1FFF69'), |
| | | opacity: 1, |
| | | speed: 10, |
| | | }) |
| | | let runningTextColor = Cesium.Color.fromCssColorString('#1FFF69') |
| | | let runningTextOffset = new Cesium.Cartesian2(0, -32) |
| | | |
| | | let pendingLineMaterial = new PolylineGlowMaterial({ |
| | | color: Cesium.Color.fromCssColorString('#FFB81D'), |
| | | }) |
| | | let pendingTextColor = Cesium.Color.fromCssColorString('#FFB81D') |
| | | let pendingTextOffset = new Cesium.Cartesian2(0, 32) |
| | | |
| | | let MapPopUpBox = HistoryDronePopup |
| | | |
| | | function getZoomFactor (camerasInfo, type) { |
| | |
| | | |
| | | return zoom_factor |
| | | } |
| | | // 记录index |
| | | let indexLog = 0 |
| | | // 颜色 |
| | | function getPendingExecutionLineColor (lineNumber) { |
| | | // 颜色顺序: 蓝(灰), 更灰 |
| | | const colors = [arrowLineMaterialPropertyGray, arrowLineMaterialPropertyGrayT] |
| | | // 计算颜色索引 (从0开始) |
| | | const colorIndex = (lineNumber - 1) % 2 |
| | | // 返回对应颜色 |
| | | return colors[colorIndex] |
| | | } |
| | | |
| | | function getLineColor (lineNumber) { |
| | | // 颜色顺序: 蓝(0), 黄(1), 绿(2) |
| | | const colors = [arrowLineMaterialProperty, arrowLineMaterialPropertyOrange, arrowLineMaterialPropertyGreen] |
| | | // 计算颜色索引 (从0开始) |
| | | const colorIndex = (lineNumber - 1) % 3 |
| | | // 返回对应颜色 |
| | | return colors[colorIndex] |
| | | } |
| | | |
| | | function getBase64Image (imgUrl) { |
| | | const img = new Image() |
| | | const canvas = document.createElement('canvas') |
| | | const ctx = canvas.getContext('2d') |
| | | |
| | | return new Promise(resolve => { |
| | | img.onload = function () { |
| | | canvas.width = img.width |
| | | canvas.height = img.height |
| | | ctx.drawImage(img, 0, 0) |
| | | const base64 = canvas.toDataURL('image/png') |
| | | resolve(base64) |
| | | } |
| | | img.src = imgUrl |
| | | }) |
| | | } |
| | | async function createCombinedSVG (index, billboardImageUrl) { |
| | | const base64Image = await getBase64Image(billboardImageUrl) |
| | | const svg = ` |
| | | <svg width="100" height="100" xmlns="http://www.w3.org/2000/svg"> |
| | | <image href="${base64Image}" x="10" y="10" width="80" height="80"/> |
| | | <text x="50" y="54" font-size="26" fill="white" text-anchor="middle" dominant-baseline="middle"> |
| | | ${index + 1} |
| | | </text> |
| | | </svg> |
| | | ` |
| | | return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}` |
| | | } |
| | | |
| | | function findHeightByCoordinates (data, targetLng, targetLat) { |
| | | // 展平二维数组,然后查找第一个匹配的点 |
| | | const allPoints = data.flat() |
| | | const point = allPoints.find(point => |
| | | point.longitude === targetLng && point.latitude === targetLat |
| | | ) |
| | | |
| | | return point ? point.height : null |
| | | } |
| | | |
| | | function renderingMachineNest (options) { |
| | | const { viewer, dockTransformPosition, dockPosition, data } = options |
| | | viewer.entities.add({ |
| | | name: 'work-drone-route-point-dock', |
| | | position: dockTransformPosition, |
| | | billboard: { |
| | | image: endingOnlineImg, |
| | | width: 50, |
| | | height: 50, |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | }, |
| | | customData: { |
| | | data, |
| | | }, |
| | | }) |
| | | viewer.entities.add({ |
| | | name: 'work-drone-route--point-polyline', |
| | | position: dockTransformPosition, |
| | | polyline: getPolyLine(viewer, { value: [dockPosition] }, 0), |
| | | }) |
| | | } |
| | | |
| | | function renderingStartingPoint (options) { |
| | | const { viewer, startPosition, data } = options |
| | | viewer.entities.add({ |
| | | name: 'work-drone-route-point-start', |
| | | position: startPosition, |
| | | billboard: { |
| | | image: rwqfdImg, |
| | | width: 50, |
| | | height: 50, |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | }, |
| | | customData: { |
| | | data, |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | export default class CreateRouteLine { |
| | | /** |
| | | * |
| | | * |
| | | * @param {*} options 参数 |
| | | */ |
| | | constructor(options) { |
| | |
| | | |
| | | /** |
| | | * 初始化viewer |
| | | * @param {*} viewer |
| | | * @param {*} viewer |
| | | */ |
| | | initCreateRoute (viewer) { |
| | | indexLog = 0 |
| | | this.viewer = viewer |
| | | } |
| | | |
| | |
| | | const currentWaypointIndex = output?.ext['current_waypoint_index'] |
| | | |
| | | this.updataMultiNestData(sn, { |
| | | currentWaypointIndex |
| | | currentWaypointIndex, |
| | | }) |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 请求航线文件数据并解析处理 |
| | | * 请求航线文件数据并绘制解析处理 |
| | | * @param {*} data 包含文件url等信息 |
| | | * @param {*} dronePosition 无人机位置 |
| | | * @returns |
| | | * @param {*} dockPosition 无人机位置 |
| | | * @returns |
| | | */ |
| | | async parsingFiles (data, dronePosition = null, isShowDock = false, isShowPointBillboard = true) { |
| | | async parsingFiles (data, dockPosition = null, isShowDock = false, isShowPointBillboard = true) { |
| | | console.log(data,'888') |
| | | const { url, wayline_type, device_sn } = data |
| | | |
| | | if (!url) return dronePosition != null ? { |
| | | dronePosition: [{ lng: dronePosition.longitude, lat: dronePosition.latitude, alt: dronePosition.height }] |
| | | } : {} |
| | | |
| | | indexLog = 0 |
| | | if (!url) |
| | | return dockPosition != null |
| | | ? { |
| | | dronePosition: [{ lng: dockPosition.longitude, lat: dockPosition.latitude, alt: dockPosition.height }], |
| | | } |
| | | : {} |
| | | this.updataMultiNestData(device_sn) |
| | | |
| | | const res = await analyzeKmzFile(`${url}?_t=${new Date().getTime()}`) |
| | |
| | | const missionConfig = removeTextKey(waylinesXmlJson.missionConfig) |
| | | const waylinesXMLObj = removeTextKey(waylinesXmlJson.Folder) |
| | | |
| | | if (templateType === "mapping3d") { |
| | | if (templateType === 'mapping3d') { |
| | | if (!waylinesXMLObj[0].Placemark.length) return ElMessage.error('没有航线点位') |
| | | } else { |
| | | if (!waylinesXMLObj.Placemark.length) return ElMessage.error('没有航线点位') |
| | | } |
| | | |
| | | if ([2, 4, 5, 6, 7, 10].includes(Number(wayline_type))) { |
| | | if (templateType === "mapping3d") { |
| | | if ([2, 4, 5, 7, 10].includes(Number(wayline_type))) { |
| | | if (templateType === 'mapping3d') { |
| | | let morePolygonData = waylinesXMLObj.map(i => ({ |
| | | data: i.Placemark |
| | | data: i.Placemark, |
| | | })) |
| | | |
| | | let newPolygonData = getNewPolygonData(morePolygonData) |
| | |
| | | waylinesXMLObj[0], |
| | | missionConfig, |
| | | templateXmlJson, |
| | | dronePosition, |
| | | dockPosition, |
| | | data, |
| | | startPoint, |
| | | isShowDock, |
| | |
| | | waylinesXMLObj, |
| | | missionConfig, |
| | | templateXmlJson, |
| | | dronePosition, |
| | | dockPosition, |
| | | data, |
| | | startPoint, |
| | | isShowDock, |
| | |
| | | return this.drawPointRoute( |
| | | waylinesXMLObj, |
| | | missionConfig, |
| | | dronePosition, |
| | | dockPosition, |
| | | data, |
| | | startPoint, |
| | | isShowDock, |
| | |
| | | } |
| | | } |
| | | |
| | | // 拆分航线 |
| | | async splitWayLine (data, dronePosition = null, isShowDock = false, isShowPointBillboard = true) { |
| | | const { |
| | | pointPlacemark, |
| | | polygonList, |
| | | pointList, |
| | | templateType, |
| | | startPoint, |
| | | execute_height_mode, |
| | | auto_flight_speed, |
| | | take_off_security_height, |
| | | buffer_distance_meters, |
| | | missionConfig, |
| | | waylinesXMLObjR, |
| | | } = await analysisPointLineKmz(`${data.domain_url}${data.object_key}`) |
| | | const { positionArray, filePositions } = this.disposeData( |
| | | waylinesXMLObjR, |
| | | missionConfig, |
| | | null, |
| | | data, |
| | | startPoint, |
| | | 0 |
| | | ) |
| | | let bigPointList = handlePointListForKmz({ |
| | | placemark: pointPlacemark, |
| | | startPoint: startPoint, |
| | | execute_height_mode: execute_height_mode, |
| | | auto_flight_speed: auto_flight_speed, |
| | | }) |
| | | // 路径线 |
| | | let entityConfig |
| | | bigPointList.shift() |
| | | let flyPositions = bigPointList.map(i => [Number(i.longitude), Number(i.latitude), Number(i.height)]) |
| | | // 渲染多个点 |
| | | let firstTopPosition = null |
| | | let resultPosotions = [] |
| | | await Promise.all(data.wayline_file_list.map(async (item, index) => { |
| | | // 小航线渲染线 |
| | | const kmzUrl = import.meta.env.VITE_APP_AIRLINE_URL + item.object_key |
| | | const { pointList } = await analysisPointLineKmz(kmzUrl) |
| | | resultPosotions.push(pointList) |
| | | })).then(res => { |
| | | // 目的是区分大航线点属于哪条小航线 |
| | | bigPointList.forEach(bigObj => { |
| | | let foundIndex = -1 |
| | | // 遍历 resultPositions 查找匹配项 |
| | | resultPosotions.some((subArray, subIndex) => { |
| | | const found = subArray.some(obj => |
| | | obj.latitude === bigObj.latitude && |
| | | obj.longitude === bigObj.longitude |
| | | ) |
| | | if (found) { |
| | | foundIndex = subIndex |
| | | return true // 找到就停止搜索 |
| | | } |
| | | return false |
| | | }) |
| | | // 如果找到了,给 bigPointList 的对象添加标记 |
| | | if (foundIndex !== -1) { |
| | | bigObj.log = foundIndex // 或 bigObj.index = foundIndex |
| | | } |
| | | }) |
| | | console.log(bigPointList, '查看结果值') // 现在 bigPointList 会有 log 属性 |
| | | bigPointList.map(async (i, index) => { |
| | | firstTopPosition = i |
| | | const position = Cesium.Cartesian3.fromDegrees(Number(i.longitude), Number(i.latitude), Number(findHeightByCoordinates(resultPosotions, i.longitude, i.latitude))) |
| | | const combinedImage = await createCombinedSVG(index, i.log % 2 ? newStartPoint : newStartPointMore) |
| | | |
| | | entityConfig = { |
| | | name: 'work-drone-route-point-split', |
| | | position: position, |
| | | billboard: { |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | image: combinedImage, |
| | | width: 50, |
| | | height: 50, |
| | | verticalOrigin: Cesium.VerticalOrigin.CENTER, |
| | | horizontalOrigin: Cesium.HorizontalOrigin.CENTER, |
| | | } |
| | | } |
| | | |
| | | // 只在第一个点添加 label |
| | | if (index === 0) { |
| | | entityConfig.label = { |
| | | text: data.task_name || '', |
| | | font: 'bold 16px sans-serif', |
| | | fillColor: data.status === 2 ? runningTextColor : pendingTextColor, |
| | | pixelOffset: data.status === 2 ? runningTextOffset : pendingTextOffset, |
| | | style: Cesium.LabelStyle.FILL_AND_OUTLINE, |
| | | outlineWidth: 2, |
| | | outlineColor: Cesium.Color.BLACK, |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | } |
| | | } |
| | | |
| | | this.viewer.entities.add(entityConfig) |
| | | }) |
| | | }) |
| | | |
| | | // 渲染航线 |
| | | data.wayline_file_list.map(async (item, index) => { |
| | | // 小航线渲染线 |
| | | const kmzUrl = import.meta.env.VITE_APP_AIRLINE_URL + item.object_key |
| | | const { pointList } = await analysisPointLineKmz(kmzUrl) |
| | | // if (curRouteLineData.value.droneData && splitLines.length === 1) { |
| | | // // 把pointList的第一个点替换为无人机位置 |
| | | // pointList[0] = curRouteLineData.value.droneData[0] |
| | | // } |
| | | if (take_off_security_height + pointList[0].height > firstTopPosition.height) { |
| | | // 安全高度+机巢高度 > 第一个点高度 |
| | | const positionTest = { |
| | | longitude: pointList[0].longitude, |
| | | latitude: pointList[0].latitude, |
| | | height: take_off_security_height + pointList[0].height, |
| | | } |
| | | |
| | | const positionTest1 = { |
| | | longitude: pointList[1].longitude, |
| | | latitude: pointList[1].latitude, |
| | | height: take_off_security_height + pointList[0].height, |
| | | } |
| | | pointList.splice(1, 0, positionTest) |
| | | // 往数组中插入元素 |
| | | pointList.splice(2, 0, positionTest1) |
| | | // console.log(pointList, 'pointList') |
| | | } else if (take_off_security_height + pointList[0].height < firstTopPosition.height) { |
| | | const positionTest = { |
| | | longitude: pointList[0].longitude, |
| | | latitude: pointList[0].latitude, |
| | | height: firstTopPosition.height, |
| | | } |
| | | pointList.splice(1, 0, positionTest) |
| | | } |
| | | const routePositions = pointList.map(i => |
| | | Cesium.Cartesian3.fromDegrees(Number(i.longitude), Number(i.latitude), Number(i.height)) |
| | | ) |
| | | |
| | | this.viewer.entities.add({ |
| | | name: 'work-drone-route-line-split', |
| | | polyline: { |
| | | width: 4, |
| | | positions: routePositions, |
| | | material: data.status === 1 ? arrowLineMaterialPropertyGray : getLineColor(index + 1), |
| | | clampToGround: false, |
| | | }, |
| | | }) |
| | | }) |
| | | // 落点线 |
| | | ; (bigPointList || [])?.forEach((item, index) => { |
| | | const topPosition = Cesium.Cartesian3.fromDegrees( |
| | | Number(item.longitude), |
| | | Number(item.latitude), |
| | | Number(item.height) |
| | | ) |
| | | let setting = { |
| | | name: 'work-drone-route--planar-polyline-split', |
| | | position: topPosition, |
| | | polyline: getPolyLine(this.viewer, { value: bigPointList }, index), |
| | | } |
| | | this.viewer.entities.add(setting) |
| | | }) |
| | | return { |
| | | entity: entityConfig, |
| | | routeLinePositions: positionArray, |
| | | filePositions: |
| | | dronePosition != null |
| | | ? [{ lng: dronePosition.longitude, lat: dronePosition.latitude, alt: dronePosition.height }, ...filePositions] |
| | | : filePositions, |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 绘制面状航线 |
| | | * @param {*} lineObj 航线文件中的 |
| | | * @param {*} missionConfig 航线文件中的 |
| | | * @param {*} templateXmlJson 航线文件中的 |
| | | * @param {*} dronePosition 无人机位置 |
| | | * @param {*} data |
| | | * @param {*} startPoint |
| | | * @param {*} isShowPointBillboard 是否显示,航线起飞点,终点等标注 |
| | | * @returns |
| | | * @param {*} dockPosition 无人机位置 |
| | | * @param {*} data |
| | | * @param {*} startPoint |
| | | * @returns |
| | | */ |
| | | async drawPlanarRoute (lineObj, missionConfig, templateXmlJson, dronePosition, data, startPoint, isShowDock, isShowPointBillboard, wayline_type) { |
| | | const { positionArray, filePositions } = this.disposeData(lineObj, missionConfig, dronePosition, data, startPoint, wayline_type) |
| | | async drawPlanarRoute ( |
| | | lineObj, |
| | | missionConfig, |
| | | templateXmlJson, |
| | | dockPosition, |
| | | data, |
| | | startPoint, |
| | | isShowDock, |
| | | isShowPointBillboard, |
| | | wayline_type |
| | | ) { |
| | | const { positionArray, filePositions } = this.disposeData( |
| | | lineObj, |
| | | missionConfig, |
| | | dockPosition, |
| | | data, |
| | | startPoint, |
| | | wayline_type |
| | | ) |
| | | const { device_sn } = data |
| | | |
| | | const droneTransformPosition = Cesium.Cartesian3.fromDegrees( |
| | | Number(dronePosition.longitude), |
| | | Number(dronePosition.latitude), |
| | | Number(dronePosition.height), |
| | | const dockTransformPosition = Cesium.Cartesian3.fromDegrees( |
| | | Number(dockPosition.longitude), |
| | | Number(dockPosition.latitude), |
| | | Number(dockPosition.height) |
| | | ) |
| | | |
| | | let coordArr = null |
| | | |
| | | const placemark = templateXmlJson.Folder?.Placemark |
| | | // 取出点位 |
| | | const coordinates = |
| | | placemark.Polygon?.outerBoundaryIs.LinearRing.coordinates?.[ |
| | | '#text' |
| | | ]?.split('\n') || [] |
| | | |
| | | const coordinates = placemark.Polygon?.outerBoundaryIs.LinearRing.coordinates?.['#text']?.split('\n') || [] |
| | | // 数组转换 |
| | | coordArr = coordinates.map((coordinate) => |
| | | coordArr = coordinates.map(coordinate => |
| | | coordinate |
| | | .replace(/\s+/g, '') |
| | | .split(',') |
| | | .map((v) => Number(v)), |
| | | .map(v => Number(v)) |
| | | ) |
| | | |
| | | // 获取当前经纬度绝对高度 |
| | | let newCoordArr = coordArr.map(item => ([item[0], item[1], 0])) |
| | | |
| | | let newCoordArr = coordArr.map(item => [item[0], item[1], 0]) |
| | | let planarRouteEntity = this.viewer.entities.add({ |
| | | name: 'work-drone-route-planar-polyline', |
| | | |
| | | polyline: { |
| | | width: 3, |
| | | positions: positionArray, |
| | |
| | | }, |
| | | |
| | | customData: { |
| | | data |
| | | } |
| | | data, |
| | | }, |
| | | }) |
| | | |
| | | let droppointPositions = (filePositions || [])?.map(i => ({ |
| | | longitude: Number(i.lng), |
| | | latitude: Number(i.lat), |
| | | height: Number(i.alt), |
| | | })); |
| | | })) |
| | | |
| | | // 落点线 |
| | | (droppointPositions || [])?.forEach((item, index) => { |
| | | const topPosition = Cesium.Cartesian3.fromDegrees( |
| | | Number(item.longitude), |
| | | Number(item.latitude), |
| | | Number(item.height) |
| | | ) |
| | | let setting = { |
| | | name: 'work-drone-route-planar-polyline', |
| | | position: topPosition, |
| | | polyline: getPolyLine(this.viewer, { value: droppointPositions }, index), |
| | | } |
| | | this.viewer.entities.add(setting) |
| | | }) |
| | | // 落点线 |
| | | ; (droppointPositions || [])?.forEach((item, index) => { |
| | | const topPosition = Cesium.Cartesian3.fromDegrees( |
| | | Number(item.longitude), |
| | | Number(item.latitude), |
| | | Number(item.height) |
| | | ) |
| | | let setting = { |
| | | name: 'work-drone-route--planar-polyline', |
| | | position: topPosition, |
| | | polyline: getPolyLine(this.viewer, { value: droppointPositions }, index), |
| | | } |
| | | this.viewer.entities.add(setting) |
| | | }) |
| | | |
| | | // 面状航线第一个点突出展示 |
| | | isShowPointBillboard && this.startingIncreasePoint(positionArray) |
| | |
| | | name: 'work-drone-route-planar-border', |
| | | |
| | | polyline: { |
| | | positions: Cesium.Cartesian3.fromDegreesArrayHeights( |
| | | cloneCoordArr.flat(), |
| | | ), |
| | | positions: Cesium.Cartesian3.fromDegreesArrayHeights(cloneCoordArr.flat()), |
| | | width: 4, |
| | | material: new Cesium.PolylineOutlineMaterialProperty({ |
| | | color: new Cesium.Color.fromBytes(74, 138, 233), |
| | |
| | | // 绘制面状地块-------------------- |
| | | this.viewer.entities.add({ |
| | | name: 'work-drone-route-planar-polygon', |
| | | |
| | | polygon: { |
| | | hierarchy: Cesium.Cartesian3.fromDegreesArrayHeights( |
| | | newCoordArr.flat(), |
| | | ), |
| | | hierarchy: Cesium.Cartesian3.fromDegreesArrayHeights(newCoordArr.flat()), |
| | | material: new Cesium.Color.fromBytes(75, 159, 221, 100), |
| | | outline: true, |
| | | outlineColor: new Cesium.Color.fromBytes(35, 85, 216, 255), |
| | |
| | | |
| | | label: { |
| | | text: data.job_name || '', |
| | | font: 'bold 16px YouSheBiaoTiHei', |
| | | font: 'bold 16px sans-serif', |
| | | fillColor: Cesium.Color.WHITE, |
| | | pixelOffset: new Cesium.Cartesian2(0, -50), |
| | | style: Cesium.LabelStyle.FILL_AND_OUTLINE, |
| | |
| | | }) |
| | | } else { |
| | | // 显示机巢位置 |
| | | if (isShowDock) { |
| | | // 起点 |
| | | this.viewer.entities.add({ |
| | | name: 'work-drone-route-point-dock', |
| | | |
| | | position: droneTransformPosition, |
| | | |
| | | billboard: { |
| | | image: endingOnlineImg, |
| | | width: 50, |
| | | height: 50, |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | }, |
| | | |
| | | customData: { |
| | | data |
| | | } |
| | | }) |
| | | |
| | | this.viewer.entities.add({ |
| | | name: 'work-drone-route--point-polyline', |
| | | position: droneTransformPosition, |
| | | polyline: getPolyLine(this.viewer, { value: [dronePosition] }, 0), |
| | | }) |
| | | } |
| | | |
| | | const startPosition = Cesium.Cartesian3.fromDegrees( |
| | | Number(filePositions[0].lng), |
| | | Number(filePositions[0].lat), |
| | | Number(filePositions[0].alt) |
| | | ) |
| | | |
| | | isShowDock && renderingMachineNest({ |
| | | viewer: this.viewer, |
| | | dockTransformPosition, |
| | | dockPosition, |
| | | data |
| | | }) |
| | | // 起点 |
| | | isShowPointBillboard && this.viewer.entities.add({ |
| | | name: 'work-drone-route-point-start', |
| | | |
| | | position: startPosition, |
| | | |
| | | billboard: { |
| | | image: rwqfdImg, |
| | | width: 50, |
| | | height: 50, |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | }, |
| | | |
| | | customData: { |
| | | data |
| | | } |
| | | isShowPointBillboard && renderingStartingPoint({ |
| | | viewer: this.viewer, |
| | | startPosition: Cesium.Cartesian3.fromDegrees( |
| | | Number(filePositions[0].lng), |
| | | Number(filePositions[0].lat), |
| | | Number(filePositions[0].alt) |
| | | ), |
| | | data |
| | | }) |
| | | } |
| | | |
| | | return { |
| | | entity: planarRouteEntity, |
| | | routeLinePositions: positionArray, |
| | | filePositions: dronePosition != null ? [ |
| | | { lng: dronePosition.longitude, lat: dronePosition.latitude, alt: dronePosition.height }, |
| | | ...filePositions |
| | | ] : filePositions |
| | | filePositions: |
| | | dockPosition != null |
| | | ? [{ lng: dockPosition.longitude, lat: dockPosition.latitude, alt: dockPosition.height }, ...filePositions] |
| | | : filePositions, |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | let setting = { |
| | | name: 'work-drone-route-planar-start', |
| | | |
| | | position: positions, |
| | | |
| | | billboard: { |
| | | image: this.planarBillboard('#2D8CF0'), |
| | | pixelOffset: new Cesium.Cartesian2(146, 72), |
| | |
| | | * 绘制点航线 |
| | | * @param {*} lineObj 航线文件中的 |
| | | * @param {*} missionConfig 航线文件中的 |
| | | * @param {*} dronePosition 无人机位置 |
| | | * @param {*} dockPosition 无人机位置 |
| | | * @param {*} data 任务信息,航线地址等 |
| | | * @param {*} startPoint |
| | | * @param {*} startPoint |
| | | * @param {*} isShowDock 是否显示机巢位置 |
| | | * @param {*} isShowPointBillboard 是否显示,航线起飞点,终点等标注 |
| | | * @returns |
| | | * @returns |
| | | */ |
| | | async drawPointRoute (lineObj, missionConfig, dronePosition, data, startPoint, isShowDock, isShowPointBillboard, wayline_type) { |
| | | const { positionArray, filePositions } = this.disposeData(lineObj, missionConfig, dronePosition, data, startPoint, wayline_type) |
| | | |
| | | const droneTransformPosition = Cesium.Cartesian3.fromDegrees( |
| | | Number(dronePosition.longitude), |
| | | Number(dronePosition.latitude), |
| | | Number(dronePosition.height), |
| | | async drawPointRoute ( |
| | | lineObj, |
| | | missionConfig, |
| | | dockPosition, |
| | | data, |
| | | startPoint, |
| | | isShowDock, |
| | | isShowPointBillboard, |
| | | wayline_type |
| | | ) { |
| | | const { positionArray, filePositions } = this.disposeData( |
| | | lineObj, |
| | | missionConfig, |
| | | dockPosition, |
| | | data, |
| | | startPoint, |
| | | wayline_type |
| | | ) |
| | | |
| | | const dockTransformPosition = Cesium.Cartesian3.fromDegrees( |
| | | Number(dockPosition.longitude), |
| | | Number(dockPosition.latitude), |
| | | Number(dockPosition.height) |
| | | ) |
| | | // 路径线 |
| | | let polyline |
| | | |
| | | |
| | | indexLog = indexLog + 1 |
| | | |
| | | if (this.type === 'clusterScheduling') { |
| | | filePositions.forEach((item, index) => { |
| | | let position = Cesium.Cartesian3.fromDegrees(item.lng, item.lat, item.alt) |
| | | |
| | | if (index === 0) { |
| | | this.viewer.entities.add({ |
| | | name: 'work-drone-route-point-start', |
| | | position, |
| | | |
| | | billboard: { |
| | | image: rwqfdImg, |
| | | outlineWidth: 0, |
| | |
| | | height: 50, |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | }, |
| | | |
| | | label: { |
| | | text: data.job_name || '', |
| | | font: 'bold 16px YouSheBiaoTiHei', |
| | | font: 'bold 16px sans-serif', |
| | | fillColor: data.type === 2 ? runningTextColor : pendingTextColor, |
| | | pixelOffset: data.type === 2 ? runningTextOffset : pendingTextOffset, |
| | | style: Cesium.LabelStyle.FILL_AND_OUTLINE, |
| | |
| | | } else if (index === filePositions.length - 1) { |
| | | this.viewer.entities.add({ |
| | | name: 'work-drone-route-point-end', |
| | | |
| | | position, |
| | | |
| | | billboard: { |
| | | image: newEndPointImg, |
| | | outlineWidth: 0, |
| | |
| | | } else { |
| | | this.viewer.entities.add({ |
| | | name: 'work-drone-route-point-approach', |
| | | |
| | | position, |
| | | |
| | | billboard: { |
| | | image: newNumPoint, |
| | | width: 50, |
| | |
| | | }) |
| | | } |
| | | }) |
| | | |
| | | polyline = this.viewer.entities.add({ |
| | | name: 'work-drone-route-point-polyline', |
| | | |
| | | polyline: { |
| | | width: 4, |
| | | positions: positionArray, |
| | | material: data.type === 2 ? runningLineMaterial : pendingLineMaterial, |
| | | material: data.type === 2 ? getLineColor(indexLog) : arrowLineMaterialPropertyGray, |
| | | clampToGround: false, |
| | | } |
| | | }, |
| | | }) |
| | | } else { |
| | | // 显示机巢位置 |
| | | if (isShowDock) { |
| | | // 起点 |
| | | this.viewer.entities.add({ |
| | | name: 'work-drone-route-point-dock', |
| | | |
| | | position: droneTransformPosition, |
| | | |
| | | billboard: { |
| | | image: endingOnlineImg, |
| | | width: 50, |
| | | height: 50, |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | }, |
| | | |
| | | customData: { |
| | | data |
| | | } |
| | | }) |
| | | |
| | | this.viewer.entities.add({ |
| | | name: 'work-drone-route--point-polyline', |
| | | position: droneTransformPosition, |
| | | polyline: getPolyLine(this.viewer, { value: [dronePosition] }, 0), |
| | | }) |
| | | } |
| | | |
| | | const startPosition = Cesium.Cartesian3.fromDegrees( |
| | | Number(filePositions[0].lng), |
| | | Number(filePositions[0].lat), |
| | | Number(filePositions[0].alt) |
| | | ) |
| | | |
| | | isShowDock && renderingMachineNest({ |
| | | viewer: this.viewer, |
| | | dockTransformPosition, |
| | | dockPosition |
| | | }) |
| | | // 起点 |
| | | isShowPointBillboard && this.viewer.entities.add({ |
| | | name: 'work-drone-route-point-start', |
| | | |
| | | position: startPosition, |
| | | |
| | | billboard: { |
| | | image: rwqfdImg, |
| | | width: 50, |
| | | height: 50, |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | }, |
| | | |
| | | customData: { |
| | | data |
| | | } |
| | | isShowPointBillboard && renderingStartingPoint({ |
| | | viewer: this.viewer, |
| | | startPosition: Cesium.Cartesian3.fromDegrees( |
| | | Number(filePositions[0].lng), |
| | | Number(filePositions[0].lat), |
| | | Number(filePositions[0].alt) |
| | | ), |
| | | data |
| | | }) |
| | | |
| | | // 终点 |
| | | isShowPointBillboard && this.viewer.entities.add({ |
| | | name: 'work-drone-route-point-end', |
| | | |
| | | position: positionArray[positionArray.length - 1], |
| | | |
| | | billboard: { |
| | | image: new Cesium.ConstantProperty(endPointImg), |
| | | width: 30, |
| | | height: 30, |
| | | verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 底部对齐 |
| | | }, |
| | | }) |
| | | |
| | | isShowPointBillboard && |
| | | this.viewer.entities.add({ |
| | | name: 'work-drone-route-point-end', |
| | | position: positionArray[positionArray.length - 1], |
| | | billboard: { |
| | | image: new Cesium.ConstantProperty(endPointImg), |
| | | width: 30, |
| | | height: 30, |
| | | verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 底部对齐 |
| | | }, |
| | | }) |
| | | console.log(data.status, '失败') |
| | | polyline = this.viewer.entities.add({ |
| | | name: 'work-drone-route-point-polyline', |
| | | |
| | | polyline: { |
| | | width: 4, |
| | | positions: positionArray, |
| | | material: getLineColor(indexLog), |
| | | material: data.status === 5 ? arrowLineMaterialPropertyGray : getLineColor(indexLog), |
| | | clampToGround: false, |
| | | }, |
| | | |
| | | customData: { |
| | | data |
| | | } |
| | | data, |
| | | }, |
| | | }) |
| | | } |
| | | |
| | |
| | | longitude: Number(i.lng), |
| | | latitude: Number(i.lat), |
| | | height: Number(i.alt), |
| | | })); |
| | | })) |
| | | |
| | | // 落点线 |
| | | (droppointPositions || [])?.forEach((item, index) => { |
| | | const topPosition = Cesium.Cartesian3.fromDegrees( |
| | | Number(item.longitude), |
| | | Number(item.latitude), |
| | | Number(item.height) |
| | | ) |
| | | // 落点线 |
| | | ; (droppointPositions || [])?.forEach((item, index) => { |
| | | const topPosition = Cesium.Cartesian3.fromDegrees( |
| | | Number(item.longitude), |
| | | Number(item.latitude), |
| | | Number(item.height) |
| | | ) |
| | | |
| | | let setting = { |
| | | name: 'work-drone-route-point-polyline', |
| | | position: topPosition, |
| | | polyline: getPolyLine(this.viewer, { value: droppointPositions }, index), |
| | | } |
| | | this.viewer.entities.add(setting) |
| | | }) |
| | | let setting = { |
| | | name: 'work-drone-route--point-polyline', |
| | | position: topPosition, |
| | | polyline: getPolyLine(this.viewer, { value: droppointPositions }, index), |
| | | } |
| | | this.viewer.entities.add(setting) |
| | | }) |
| | | |
| | | return { |
| | | entity: polyline, |
| | | routeLinePositions: positionArray, |
| | | filePositions: dronePosition != null ? [ |
| | | { lng: dronePosition.longitude, lat: dronePosition.latitude, alt: dronePosition.height }, |
| | | ...filePositions |
| | | ] : filePositions, |
| | | filePositions: |
| | | dockPosition !== null |
| | | ? [{ lng: dockPosition.longitude, lat: dockPosition.latitude, alt: dockPosition.height }, ...filePositions] |
| | | : filePositions, |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 通用处理数据 |
| | | * @param {*} lineObj |
| | | * @param {*} missionConfig |
| | | * @param {*} dronePosition |
| | | * @param {*} startPoint |
| | | * @param {*} lineObj |
| | | * @param {*} missionConfig |
| | | * @param {*} dronePosition |
| | | * @param {*} startPoint |
| | | * @returns {} {positionArray: 带拼接点位置, filePositions:光航线点位置} |
| | | */ |
| | | disposeData (lineObj, missionConfig, dronePosition, data, startPoint, wayline_type) { |
| | | indexLog = indexLog + 1 |
| | | const { device_sn } = data |
| | | |
| | | const executeHeightMode = lineObj.executeHeightMode === "WGS84" |
| | | const executeHeightMode = lineObj.executeHeightMode === 'WGS84' |
| | | |
| | | let filePositions = lineObj.Placemark.map(item => { |
| | | const [lng, lat] = item.Point.coordinates.split(',') |
| | |
| | | let safetyHeight |
| | | |
| | | if (executeHeightMode) { |
| | | let startHeight = new Decimal(missionConfig.takeOffSecurityHeight) |
| | | .plus(dronePosition.height).toNumber() |
| | | let startHeight = new Decimal(missionConfig.takeOffSecurityHeight).plus(dronePosition?.height || 0).toNumber() |
| | | |
| | | safetyHeight = startHeight < filePositions[0].alt ? filePositions[0].alt : startHeight |
| | | } else { |
| | | safetyHeight = missionConfig.takeOffSecurityHeight < filePositions[0].alt ? filePositions[0].alt : missionConfig.takeOffSecurityHeight |
| | | safetyHeight = |
| | | missionConfig.takeOffSecurityHeight < filePositions[0].alt |
| | | ? filePositions[0].alt |
| | | : missionConfig.takeOffSecurityHeight |
| | | } |
| | | |
| | | let safetyPositions = [] |
| | |
| | | }) |
| | | |
| | | this.updataMultiNestData(device_sn, { |
| | | currentRoutePositions |
| | | currentRoutePositions, |
| | | }) |
| | | |
| | | if ([0].includes(wayline_type)) { |
| | | let startHeight = executeHeightMode ? filePositions[0].alt : filePositions[0].alt + (dronePosition != null ? dronePosition.height : 0) |
| | | let endHeight = executeHeightMode ? filePositions[1].alt : filePositions[1].alt + (dronePosition != null ? dronePosition.height : 0) |
| | | let startHeight = executeHeightMode |
| | | ? filePositions[0].alt |
| | | : filePositions[0].alt + (dronePosition != null ? dronePosition.height : 0) |
| | | let endHeight = executeHeightMode |
| | | ? filePositions[1].alt |
| | | : filePositions[1].alt + (dronePosition != null ? dronePosition.height : 0) |
| | | |
| | | this.updataMultiNestData(device_sn, { |
| | | showPopupPosition: Cesium.Cartesian3.midpoint( |
| | | Cesium.Cartesian3.fromDegrees(filePositions[0].lng, filePositions[0].lat, startHeight), |
| | | Cesium.Cartesian3.fromDegrees(filePositions[1].lng, filePositions[1].lat, endHeight), |
| | | new Cesium.Cartesian3()) |
| | | new Cesium.Cartesian3() |
| | | ), |
| | | }) |
| | | } else { |
| | | let startHeight = executeHeightMode ? safetyPositions[1].alt : safetyPositions[1].alt + (dronePosition != null ? dronePosition.height : 0) |
| | | let endHeight = executeHeightMode ? safetyPositions[2].alt : safetyPositions[2].alt + (dronePosition != null ? dronePosition.height : 0) |
| | | let startHeight = executeHeightMode |
| | | ? safetyPositions[1].alt |
| | | : safetyPositions[1].alt + (dronePosition != null ? dronePosition.height : 0) |
| | | let endHeight = executeHeightMode |
| | | ? safetyPositions[2].alt |
| | | : safetyPositions[2].alt + (dronePosition != null ? dronePosition.height : 0) |
| | | |
| | | this.updataMultiNestData(device_sn, { |
| | | showPopupPosition: Cesium.Cartesian3.midpoint( |
| | | Cesium.Cartesian3.fromDegrees(safetyPositions[1].lng, safetyPositions[1].lat, startHeight), |
| | | Cesium.Cartesian3.fromDegrees(safetyPositions[2].lng, safetyPositions[2].lat, endHeight), |
| | | new Cesium.Cartesian3()) |
| | | new Cesium.Cartesian3() |
| | | ), |
| | | }) |
| | | } |
| | | |
| | |
| | | |
| | | return { |
| | | ...item, |
| | | alt: Number(height) |
| | | alt: Number(height), |
| | | } |
| | | }) |
| | | }), |
| | | } |
| | | } |
| | | |
| | |
| | | return i?.name && i?.name.includes('work-drone-route-') |
| | | }) |
| | | |
| | | Array.isArray(entities) && entities.forEach(item => { |
| | | this.viewer?.entities.remove(item) |
| | | }) |
| | | Array.isArray(entities) && |
| | | entities.forEach(item => { |
| | | this.viewer?.entities.remove(item) |
| | | }) |
| | | } |
| | | |
| | | // 当前无人机视椎体相关 |
| | |
| | | ['wide', 'ir', 'zoom'].includes(curDroneData?.camera_type) |
| | | ) { |
| | | if (curDroneData.camera_type === 'wide') { |
| | | fov = 60 |
| | | fov = 65 |
| | | } else { |
| | | const diffFocal = zoomFactor.value[curDroneData.camera_type].max - getZoomFactor(host?.cameras?.[0], curDroneData.camera_type) |
| | | |
| | | fov = (60 / zoomFactor.value[curDroneData.camera_type].max) * (diffFocal === 0 ? 1 : diffFocal) |
| | | const cameraMultiplier = getZoomFactor(host?.cameras?.[0], curDroneData.camera_type) |
| | | switch (cameraMultiplier) { |
| | | case 2: fov = 30; break |
| | | case 3: fov = 19; break |
| | | case 4: fov = 15; break |
| | | case 5: fov = 13; break |
| | | case 6: fov = 11; break |
| | | case 7: fov = 9; break |
| | | case 8: fov = 8.34; break |
| | | case 9: fov = 7.68; break |
| | | case 10: fov = 6.9; break |
| | | case 14: fov = 5; break |
| | | default: fov = 55.946 * Math.exp(-0.4504 * cameraMultiplier) + 6.323 |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | pitch: 0, |
| | | heading: attitude_head, |
| | | |
| | | isShowVideoPlan: this.isShowVideoPlan |
| | | }) |
| | | isShowVideoPlan: this.isShowVideoPlan, |
| | | }), |
| | | }) |
| | | } |
| | | } |
| | |
| | | |
| | | if (device_sn && live_status) { |
| | | this.updataMultiNestData(device_sn, { |
| | | camera_type: live_status?.[0].video_type || 'wide' |
| | | camera_type: live_status?.[0].video_type || 'wide', |
| | | }) |
| | | } |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | // 设置当前无人机视椎体信息面板 |
| | | setFrustumInfoDetails (data) { |
| | | const { |
| | | dock_sn, |
| | | distance_to_airport, |
| | | distance_to_next_point, |
| | | estimated_time_to_next_point, |
| | | plane_mileage |
| | | } = data |
| | | const { dock_sn, distance_to_airport, distance_to_next_point, estimated_time_to_next_point, plane_mileage } = data |
| | | |
| | | if (dock_sn) { |
| | | const aircraftEntity = this.viewer?.entities.values.find(i => { |
| | |
| | | } |
| | | |
| | | aircraftEntity.label = new Cesium.LabelGraphics({ |
| | | text: `离机场平面距离:${distance_to_airport}m\n离下个航点平面距离:${Math.round(distance_to_next_point)}m\n到达下个航点剩余时间:${arrivalTime}\n航线总平面里程:${Math.round(plane_mileage)}m`, |
| | | text: `离机场平面距离:${distance_to_airport}m\n离下个航点平面距离:${Math.round( |
| | | distance_to_next_point |
| | | )}m\n到达下个航点剩余时间:${arrivalTime}\n航线总平面里程:${Math.round(plane_mileage)}m`, |
| | | font: '13px monospace', |
| | | showBackground: true, |
| | | horizontalOrigin: Cesium.HorizontalOrigin.CENTER, |
| | |
| | | |
| | | return |
| | | } |
| | | |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | // 设置无人机模型 |
| | | setAircraftGltf (host) { |
| | | const parent_sn = host?.parent_sn |
| | | const attitude_head = Number(host?.payloads?.[0]?.gimbal_yaw) - 90 || 0 |
| | | |
| | | if (parent_sn) { |
| | | const aircraftEntity = this.viewer?.entities.values.find(i => { |
| | |
| | | }) |
| | | |
| | | const position = Cesium.Cartesian3.fromDegrees(host?.longitude, host?.latitude, host?.height) |
| | | // ✅ 新增:Heading / Pitch / Roll(单位:度 -> 弧度) |
| | | const heading = Cesium.Math.toRadians(attitude_head) // 航向角 |
| | | const pitch = Cesium.Math.toRadians(0) // 俯仰角 |
| | | const roll = Cesium.Math.toRadians(0) // 横滚角 |
| | | |
| | | // ✅ 新增:构造 HeadingPitchRoll 和 四元数 |
| | | const hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll) |
| | | const orientation = Cesium.Transforms.headingPitchRollQuaternion(position, hpr) |
| | | |
| | | if (aircraftEntity) { |
| | | aircraftEntity.position = new Cesium.ConstantPositionProperty(position) |
| | | aircraftEntity.orientation = new Cesium.ConstantProperty(orientation) |
| | | |
| | | return |
| | | } |
| | |
| | | this.viewer?.entities.add({ |
| | | name: `aircraft-glf-${parent_sn}`, |
| | | position, |
| | | orientation, |
| | | label: {}, |
| | | model: { |
| | | uri: aircraftGltf, // 或 .glb |
| | | scale: 1.0, // 缩放比例 |
| | | scale: 64, // 缩放比例 |
| | | minimumPixelSize: 64, // 最小像素尺寸(保证模型远处可见) |
| | | maximumScale: 128, // 最大缩放(可选) |
| | | maximumScale: 64, // 最大缩放(可选) |
| | | }, |
| | | }) |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 移除无人机模型 |
| | | removeAircraftGltf (host) { |
| | | const parent_sn = host?.parent_sn |
| | | |
| | | if (parent_sn) { |
| | | const aircraftEntity = this.viewer?.entities.values.find(i => { |
| | | return i?.name && i?.name.includes(`aircraft-glf-${parent_sn}`) |
| | | }) |
| | | |
| | | if (aircraftEntity) { |
| | | this.viewer?.entities.remove(aircraftEntity) |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 移除所有无人机视椎体 |
| | | removeViewInfoFrustum () { |
| | | this.multiNestData.forEach(item => { |
| | | if ('viewInfoFrustum' in item && item.viewInfoFrustum) { |
| | |
| | | return i?.name && i?.name.includes(`aircraft-glf-${parent_sn}`) |
| | | }) |
| | | |
| | | Array.isArray(entities) && entities.forEach(item => { |
| | | this.viewer?.entities.remove(item) |
| | | }) |
| | | Array.isArray(entities) && |
| | | entities.forEach(item => { |
| | | this.viewer?.entities.remove(item) |
| | | }) |
| | | } |
| | | } |
| | | |
| | |
| | | return i?.name && i?.name.includes(`aircraft-glf-`) |
| | | }) |
| | | |
| | | Array.isArray(entities) && entities.forEach(item => { |
| | | this.viewer?.entities.remove(item) |
| | | }) |
| | | Array.isArray(entities) && |
| | | entities.forEach(item => { |
| | | this.viewer?.entities.remove(item) |
| | | }) |
| | | |
| | | this.multiNestData.length > 0 && (this.multiNestData.forEach(item => { |
| | | item.viewInfoFrustum?.clear() |
| | | })) |
| | | this.multiNestData.length > 0 && |
| | | this.multiNestData.forEach(item => { |
| | | item.viewInfoFrustum?.clear() |
| | | }) |
| | | } |
| | | |
| | | /** |
| | |
| | | this.multiNestData[isHaveInd] = { |
| | | ...this.multiNestData[isHaveInd], |
| | | |
| | | ...data |
| | | ...data, |
| | | } |
| | | } else { |
| | | this.multiNestData.push({ |
| | | device_sn, |
| | | ...data |
| | | ...data, |
| | | }) |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 根据机巢sn获取当前机巢对应的相关信息 |
| | | * @param {*} device_sn |
| | | * @param {*} device_sn |
| | | */ |
| | | getCurDroneData (device_sn) { |
| | | const isHave = this.multiNestData.find(item => item.device_sn === device_sn) |
| | |
| | | return { |
| | | ...item, |
| | | show: true, |
| | | uuid: uuidv4() |
| | | uuid: uuidv4(), |
| | | } |
| | | }) |
| | | this.viewer.scene.postRender.addEventListener(that.labelBoxRenderHandler) |
| | | } |
| | | |
| | | showSingleDetailMapPopup (data) { |
| | | this.detailsMapPopupData.forEach(i => i.job_id === data.job_id && i.device_sn === data.device_sn && (i.show = true)) |
| | | this.detailsMapPopupData.forEach( |
| | | i => i?.job_id === data?.job_id && i?.device_sn === data?.device_sn && (i.show = true) |
| | | ) |
| | | } |
| | | |
| | | getLabelDom (data) { |
| | |
| | | tooltipContainer.style.position = 'absolute' |
| | | tooltipContainer.style.transform = 'translate(-50%,10%)' |
| | | tooltipContainer.style.pointerEvents = 'none' |
| | | document.querySelector('.content-map-popups')?.append(tooltipContainer) |
| | | document.querySelector('.content-map-popups').append(tooltipContainer) |
| | | render(vNode, tooltipContainer) |
| | | return tooltipContainer |
| | | } |
| | | |
| | | labelBoxRender () { |
| | | this.detailsMapPopupData.filter(i => i.show === true).forEach(item => { |
| | | const { showPopupPosition } = this.getCurDroneData(item.device_sn) |
| | | this.detailsMapPopupData |
| | | .filter(i => i.show === true) |
| | | .forEach(item => { |
| | | const { showPopupPosition } = this.getCurDroneData(item.device_sn) |
| | | |
| | | let dom = document.querySelector(`[id="${item.uuid}"]`) |
| | | let dom = document.querySelector(`[id="${item.uuid}"]`) |
| | | |
| | | if (!dom) { |
| | | dom = this.getLabelDom(item) |
| | | } |
| | | if (!dom) { |
| | | dom = this.getLabelDom(item) |
| | | } |
| | | |
| | | const screenPosition = this.viewer?.scene.cartesianToCanvasCoordinates(showPopupPosition) |
| | | if (screenPosition) { |
| | | dom.style.left = `${screenPosition.x}px` |
| | | dom.style.top = `${screenPosition.y}px` |
| | | dom.style.display = 'block' |
| | | } |
| | | }) |
| | | const screenPosition = this.viewer?.scene.cartesianToCanvasCoordinates(showPopupPosition) |
| | | if (screenPosition) { |
| | | dom.style.left = `${screenPosition.x}px` |
| | | dom.style.top = `${screenPosition.y}px` |
| | | dom.style.display = 'block' |
| | | } |
| | | }) |
| | | } |
| | | |
| | | // 移除弹框标签 |
| | |
| | | const that = this |
| | | that.viewer?.scene.postRender.removeEventListener(that.labelBoxRenderHandler) |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * @Author : yuan |
| | | * @Date : 2025-08-16 17:01:46 |
| | | * @LastEditors : yuan |
| | | * @LastEditTime : 2025-09-26 16:27:20 |
| | | * @FilePath : \src\utils\cesium\frustum\CesiumVideoFrustum.js |
| | | * @Description : |
| | | * Copyright 2025 OBKoro1, All Rights Reserved. |
| | | * 2025-08-16 17:01:46 |
| | | */ |
| | | import * as Cesium from 'cesium' |
| | | |
| | | var videoShed3dShader = ` |
| | | uniform float ztzf_frustum_opacity; |
| | | |
| | | uniform sampler2D ztzf_frustum_videoTexture; |
| | | uniform sampler2D ztzf_frustum_maskTexture; |
| | | uniform vec4 ztzf_frustum_hiddenAreaColor; |
| | | |
| | | uniform sampler2D shadowMap_texture; |
| | | uniform mat4 shadowMap_matrix; |
| | | uniform vec4 shadowMap_lightPositionEC; |
| | | uniform vec4 shadowMap_texelSizeDepthBias; |
| | | uniform vec4 shadowMap_normalOffsetScale; |
| | | uniform bool ztzf_frustum_flipx; |
| | | uniform bool ztzf_frustum_flipy; |
| | | |
| | | // 新增边框参数 |
| | | uniform vec4 ztzf_frustum_borderColor; // 边框颜色(RGBA) |
| | | uniform float ztzf_frustum_borderWidth; // 边框宽度(0.0-1.0) |
| | | |
| | | uniform sampler2D colorTexture; |
| | | uniform sampler2D depthTexture; |
| | | in vec2 v_textureCoordinates; |
| | | |
| | | vec4 toEye(in vec2 uv, in float depth) { |
| | | vec2 xy = vec2((uv.x * 2.0 - 1.0), (uv.y * 2.0 - 1.0)); |
| | | vec4 posInCamera = czm_inverseProjection * vec4(xy, depth, 1.0); |
| | | posInCamera = posInCamera / posInCamera.w; |
| | | return posInCamera; |
| | | } |
| | | |
| | | float getDepthMars3D(in vec4 depth) { |
| | | float z_window = czm_unpackDepth(depth); |
| | | z_window = czm_reverseLogDepth(z_window); |
| | | float n_range = czm_depthRange.near; |
| | | float f_range = czm_depthRange.far; |
| | | return (2.0 * z_window - n_range - f_range) / (f_range - n_range); |
| | | } |
| | | float _czm_sampleShadowMap(sampler2D shadowMap, vec2 uv) { |
| | | return texture(shadowMap, uv).r; |
| | | } |
| | | float _czm_shadowDepthCompare(sampler2D shadowMap, vec2 uv, float depth) { |
| | | return step(depth, _czm_sampleShadowMap(shadowMap, uv)); |
| | | } |
| | | float _czm_shadowVisibility(sampler2D shadowMap, czm_shadowParameters shadowParameters) { |
| | | float depthBias = shadowParameters.depthBias; |
| | | float depth = shadowParameters.depth; |
| | | float nDotL = shadowParameters.nDotL; |
| | | float normalShadingSmooth = shadowParameters.normalShadingSmooth; |
| | | float darkness = shadowParameters.darkness; |
| | | vec2 uv = shadowParameters.texCoords; |
| | | depth -= depthBias; |
| | | vec2 texelStepSize = shadowParameters.texelStepSize; |
| | | float radius = 1.0; |
| | | float dx0 = -texelStepSize.x * radius; |
| | | float dy0 = -texelStepSize.y * radius; |
| | | float dx1 = texelStepSize.x * radius; |
| | | float dy1 = texelStepSize.y * radius; |
| | | float visibility = (_czm_shadowDepthCompare(shadowMap, uv, depth) + |
| | | _czm_shadowDepthCompare(shadowMap, uv + vec2(dx0, dy0), depth) + |
| | | _czm_shadowDepthCompare(shadowMap, uv + vec2(0.0, dy0), depth) + |
| | | _czm_shadowDepthCompare(shadowMap, uv + vec2(dx1, dy0), depth) + |
| | | _czm_shadowDepthCompare(shadowMap, uv + vec2(dx0, 0.0), depth) + |
| | | _czm_shadowDepthCompare(shadowMap, uv + vec2(dx1, 0.0), depth) + |
| | | _czm_shadowDepthCompare(shadowMap, uv + vec2(dx0, dy1), depth) + |
| | | _czm_shadowDepthCompare(shadowMap, uv + vec2(0.0, dy1), depth) + |
| | | _czm_shadowDepthCompare(shadowMap, uv + vec2(dx1, dy1), depth)) * (1.0 / 9.0); |
| | | return visibility; |
| | | } |
| | | |
| | | vec3 pointProjectOnPlane(in vec3 planeNormal, in vec3 planeOrigin, in vec3 point) { |
| | | vec3 v01 = point - planeOrigin; |
| | | float d = dot(planeNormal, v01); |
| | | return (point - planeNormal * d); |
| | | } |
| | | float ptm(vec3 pt) { |
| | | return sqrt(pt.x * pt.x + pt.y * pt.y + pt.z * pt.z); |
| | | } |
| | | |
| | | void main() { |
| | | const float PI = 3.141592653589793; |
| | | vec4 color = texture(colorTexture, v_textureCoordinates); |
| | | vec4 currD = texture(depthTexture, v_textureCoordinates); |
| | | |
| | | float depth = getDepthMars3D(currD); |
| | | vec4 positionEC = toEye(v_textureCoordinates, depth); |
| | | vec3 normalEC = vec3(1.0); |
| | | czm_shadowParameters shadowParameters; |
| | | shadowParameters.texelStepSize = shadowMap_texelSizeDepthBias.xy; |
| | | shadowParameters.depthBias = shadowMap_texelSizeDepthBias.z; |
| | | shadowParameters.normalShadingSmooth = shadowMap_texelSizeDepthBias.w; |
| | | shadowParameters.darkness = shadowMap_normalOffsetScale.w; |
| | | shadowParameters.depthBias *= max(depth * 0.01, 1.0); |
| | | |
| | | vec3 directionEC = normalize(positionEC.xyz - shadowMap_lightPositionEC.xyz); |
| | | float nDotL = clamp(dot(normalEC, -directionEC), 0.0, 1.0); |
| | | vec4 shadowPosition = shadowMap_matrix * positionEC; |
| | | shadowPosition /= shadowPosition.w; |
| | | if(any(lessThan(shadowPosition.xyz, vec3(0.0))) || any(greaterThan(shadowPosition.xyz, vec3(1.0)))) { |
| | | out_FragColor = color; |
| | | return; |
| | | } |
| | | |
| | | shadowParameters.texCoords = shadowPosition.xy; |
| | | shadowParameters.depth = shadowPosition.z; |
| | | shadowParameters.nDotL = nDotL; |
| | | float visibility = _czm_shadowVisibility(shadowMap_texture, shadowParameters); |
| | | |
| | | //视频投射 |
| | | if(visibility == 1.0) { |
| | | if(ztzf_frustum_flipx){ |
| | | shadowPosition.x = shadowPosition.x + (0.5 - shadowPosition.x) * 2.0; |
| | | } |
| | | if(ztzf_frustum_flipy){ |
| | | shadowPosition.y = shadowPosition.y + (0.5 - shadowPosition.y) * 2.0; |
| | | } |
| | | |
| | | vec4 videoColor = texture(ztzf_frustum_videoTexture, shadowPosition.xy); |
| | | vec4 maskColor = texture(ztzf_frustum_maskTexture, shadowPosition.xy); |
| | | videoColor *= maskColor; |
| | | |
| | | // 计算到边缘的距离 |
| | | float distToEdge = min( |
| | | min(shadowPosition.x, 1.0 - shadowPosition.x), |
| | | min(shadowPosition.y, 1.0 - shadowPosition.y) |
| | | ); |
| | | |
| | | // 混合视频颜色和背景 |
| | | vec4 finalColor = mix(color, vec4(videoColor.xyz, 1.0), ztzf_frustum_opacity * videoColor.a); |
| | | |
| | | // 添加边框效果 |
| | | if(distToEdge < ztzf_frustum_borderWidth && videoColor.a > 0.0) { |
| | | float borderFactor = smoothstep(0.0, ztzf_frustum_borderWidth, distToEdge); |
| | | finalColor = mix(ztzf_frustum_borderColor, finalColor, borderFactor); |
| | | } |
| | | |
| | | out_FragColor = finalColor; |
| | | } else { |
| | | if(abs(shadowPosition.z - 0.0) < 0.01) { |
| | | return; |
| | | } |
| | | out_FragColor = vec4(mix(color.rgb, ztzf_frustum_hiddenAreaColor.rgb, ztzf_frustum_hiddenAreaColor.a), ztzf_frustum_hiddenAreaColor.a); |
| | | } |
| | | } |
| | | ` |
| | | |
| | | class CesiumVideoFrustum { |
| | | constructor(viewer, param) { |
| | | this.param = param |
| | | |
| | | var option = this._initCameraParam() |
| | | |
| | | if (option || (option = {}), this.viewer = viewer, this._cameraPosition = option.cameraPosition, this._position = option.position, |
| | | this.type = option.type, this._alpha = option.alpha || 0, this.element = option.element, this.color = option.color, |
| | | this._debugFrustum = Cesium.defaultValue(option.debugFrustum, !0), this._aspectRatio = option.aspectRatio || this._getWinWidHei(), |
| | | this._camerafov = option.fov || Cesium.Math.toDegrees(this.viewer.scene.camera.frustum.fov), |
| | | this._isShadowMap = option.isShadowMap || false, |
| | | this.texture = option.texture || new Cesium.Texture({ |
| | | context: this.viewer.scene.context, |
| | | source: { |
| | | width: 1, |
| | | height: 1, |
| | | arrayBufferView: new Uint8Array([255, 255, 255, 255]) |
| | | }, |
| | | flipY: !1 |
| | | }), this.defaultShow = Cesium.defaultValue(option.show, !0), !this.cameraPosition || !this.position) return void console.log('初始化失败:请确认相机位置与视点位置正确!') |
| | | |
| | | this.activeVideo() |
| | | this._createShadowMap() |
| | | this._getOrientation() |
| | | this._addCameraFrustum() |
| | | this._addPostProcess() |
| | | this.viewer.scene.primitives.add(this) |
| | | } |
| | | |
| | | get alpha () { |
| | | return this._alpha |
| | | } |
| | | |
| | | set alpha (e) { |
| | | this._alpha = e |
| | | this._changeViewPos() |
| | | } |
| | | |
| | | get aspectRatio () { |
| | | return this._aspectRatio |
| | | } |
| | | |
| | | set aspectRatio (e) { |
| | | this._aspectRatio = e |
| | | this._changeVideoWidHei() |
| | | } |
| | | |
| | | get debugFrustum () { |
| | | return this._debugFrustum |
| | | } |
| | | |
| | | set debugFrustum (e) { |
| | | this._debugFrustum = e |
| | | this.cameraFrustum.show = e |
| | | this.cameraFrustumOutline.show = e |
| | | } |
| | | |
| | | get fov () { |
| | | return this._camerafov |
| | | } |
| | | |
| | | set fov (e) { |
| | | this._camerafov = e |
| | | this._changeCameraFov() |
| | | } |
| | | |
| | | get cameraPosition () { |
| | | return this._cameraPosition |
| | | } |
| | | |
| | | set cameraPosition (e) { |
| | | e && (this._cameraPosition = e, this._changeCameraPos()) |
| | | } |
| | | |
| | | get isShadowMap () { |
| | | return this._isShadowMap |
| | | } |
| | | |
| | | set isShadowMap (e) { |
| | | this._isShadowMap = e |
| | | this._changeCameraIsShadowMap() |
| | | } |
| | | |
| | | get position () { |
| | | return this._position |
| | | } |
| | | |
| | | set position (e) { |
| | | e && (this._position = e, this._changeViewPos()) |
| | | } |
| | | |
| | | get params () { |
| | | var t = {} |
| | | return t.element = this.element, |
| | | t.position = this.position, |
| | | t.cameraPosition = this.cameraPosition, |
| | | t.fov = this.fov, |
| | | t.isShadowMap = this.isShadowMap, |
| | | t.aspectRatio = this.aspectRatio, |
| | | t.alpha = this.alpha, |
| | | t.debugFrustum = this.debugFrustum, |
| | | t.show = this.show |
| | | } |
| | | |
| | | get show () { |
| | | return this.defaultShow |
| | | } |
| | | |
| | | set show (e) { |
| | | this.defaultShow = Boolean(e), |
| | | this._switchShow() |
| | | } |
| | | |
| | | isDestroyed () { |
| | | return false |
| | | } |
| | | |
| | | _initCameraParam () { |
| | | var cameraPosition = Cesium.Cartesian3.fromDegrees(this.param.position.x * 1, this.param.position.y * 1, this.param.position.z * 1) |
| | | |
| | | const hpr = Cesium.HeadingPitchRoll.fromDegrees(this.param.rotation.y * 1, this.param.rotation.x * 1, 0) |
| | | const orientation = Cesium.Transforms.headingPitchRollQuaternion(cameraPosition, hpr) |
| | | |
| | | let matrix = Cesium.Matrix3.fromQuaternion( |
| | | orientation, |
| | | new Cesium.Matrix3() |
| | | ) |
| | | |
| | | let direction = Cesium.Matrix3.getColumn(matrix, 2, new Cesium.Cartesian3()) |
| | | |
| | | let frontDirect = Cesium.Cartesian3.multiplyByScalar( |
| | | direction, |
| | | 250, |
| | | new Cesium.Cartesian3() |
| | | ) |
| | | let position = Cesium.Cartesian3.add(cameraPosition, frontDirect, new Cesium.Cartesian3()) |
| | | |
| | | return { |
| | | cameraPosition: cameraPosition, |
| | | position: position, |
| | | alpha: this.param.alpha, |
| | | near: this.param.near, |
| | | fov: this.param.fov, |
| | | isShadowMap: this.param.isShadowMap, |
| | | debugFrustum: this.param.debugFrustum, |
| | | aspectRatio: this.param.aspectRatio, |
| | | element: this.param.element, |
| | | } |
| | | } |
| | | |
| | | _changeAlpha (e) { |
| | | this.alpha = e |
| | | } |
| | | |
| | | _changeAspectRatio (e) { |
| | | this.aspectRatio = e |
| | | } |
| | | |
| | | _changeRotation (e) { |
| | | if (e) { |
| | | this.param.rotation = e |
| | | var option = this._initCameraParam() |
| | | this.position = option.position |
| | | } |
| | | } |
| | | |
| | | _changeCameraPosition (e) { |
| | | if (e) { |
| | | this.param.position = e |
| | | var option = this._initCameraParam() |
| | | this.cameraPosition = option.cameraPosition |
| | | } |
| | | } |
| | | |
| | | _changeFov (e) { |
| | | if (e) { |
| | | this.param.fov = e |
| | | var option = this._initCameraParam() |
| | | this.fov = option.fov |
| | | } |
| | | } |
| | | |
| | | _changeIsShadowMap (e) { |
| | | this.param.isShadowMap = e |
| | | var option = this._initCameraParam() |
| | | this.isShadowMap = option.isShadowMap |
| | | } |
| | | |
| | | _changeFar (e) { |
| | | if (e) { |
| | | this.param.far = e |
| | | var option = this._initCameraParam() |
| | | this.position = option.position |
| | | } |
| | | } |
| | | |
| | | _changeNear (e) { |
| | | if (e) { |
| | | this.param.near = e |
| | | this.near = this.param.near |
| | | this._changeCameraPos() |
| | | } |
| | | } |
| | | |
| | | _getWinWidHei () { |
| | | var viewer = this.viewer.scene |
| | | return viewer.canvas.clientWidth / viewer.canvas.clientHeight |
| | | } |
| | | |
| | | _changeCameraFov () { |
| | | this.postProcess && this.viewer.scene.postProcessStages.remove(this.postProcess) |
| | | |
| | | this.viewer.scene.primitives.remove(this.cameraFrustum), |
| | | this.viewer.scene.primitives.remove(this.cameraFrustumOutline), |
| | | this._createShadowMap(this.cameraPosition, this.position), |
| | | this._getOrientation(), |
| | | this._addCameraFrustum(), |
| | | this._addPostProcess() |
| | | } |
| | | |
| | | _changeCameraIsShadowMap () { |
| | | this.postProcess && this.viewer.scene.postProcessStages.remove(this.postProcess) |
| | | |
| | | this.viewShadowMap && this.viewShadowMap.destroy() |
| | | this.viewShadowMap = null |
| | | |
| | | this.viewer.scene.primitives.remove(this.cameraFrustum), |
| | | this.viewer.scene.primitives.remove(this.cameraFrustumOutline), |
| | | this._createShadowMap(this.cameraPosition, this.position), |
| | | this._getOrientation(), |
| | | this._addCameraFrustum(), |
| | | this._addPostProcess() |
| | | } |
| | | |
| | | _changeVideoWidHei () { |
| | | this.postProcess && this.viewer.scene.postProcessStages.remove(this.postProcess) |
| | | |
| | | this.viewer.scene.primitives.remove(this.cameraFrustum), |
| | | this.viewer.scene.primitives.remove(this.cameraFrustumOutline), |
| | | this._createShadowMap(this.cameraPosition, this.position), |
| | | this._getOrientation(), |
| | | this._addCameraFrustum(), |
| | | this._addPostProcess() |
| | | } |
| | | |
| | | _changeCameraPos () { |
| | | this.postProcess && this.viewer.scene.postProcessStages.remove(this.postProcess) |
| | | |
| | | this.viewShadowMap && this.viewShadowMap.destroy() |
| | | this.viewShadowMap = null |
| | | |
| | | this.viewer.scene.primitives.remove(this.cameraFrustum), |
| | | this.viewer.scene.primitives.remove(this.cameraFrustumOutline), |
| | | // this.cameraFrustum.destroy(), |
| | | this._createShadowMap(this.cameraPosition, this.position), |
| | | this._getOrientation(), |
| | | this._addCameraFrustum(), |
| | | this._addPostProcess() |
| | | } |
| | | |
| | | _changeViewPos () { |
| | | this.postProcess && this.viewer.scene.postProcessStages.remove(this.postProcess) |
| | | |
| | | this.viewShadowMap && this.viewShadowMap.destroy() |
| | | this.viewShadowMap = null |
| | | |
| | | this.viewer.scene.primitives.remove(this.cameraFrustum), |
| | | this.viewer.scene.primitives.remove(this.cameraFrustumOutline), |
| | | // this.cameraFrustum.destroy(), |
| | | this._createShadowMap(this.cameraPosition, this.position), |
| | | this._getOrientation(), |
| | | this._addCameraFrustum(), |
| | | this._addPostProcess() |
| | | } |
| | | |
| | | _switchShow () { |
| | | this.show ? !this.postProcess && this._addPostProcess() : (this.postProcess && this.viewer.scene.postProcessStages.remove(this.postProcess), delete this.postProcess, this.postProcess = null), |
| | | this.cameraFrustum.show = this.show, |
| | | this.cameraFrustumOutline.show = this.show |
| | | } |
| | | |
| | | activeVideo () { |
| | | var video = this.element, |
| | | that = this |
| | | if (video) { |
| | | var viewer = this.viewer |
| | | this.activeVideoListener || (this.activeVideoListener = function () { |
| | | that.videoTexture && that.videoTexture.destroy(), |
| | | that.videoTexture = new Cesium.Texture({ |
| | | context: viewer.scene.context, |
| | | source: video, |
| | | width: 1, |
| | | height: 1, |
| | | pixelFormat: Cesium.PixelFormat.RGBA, |
| | | pixelDatatype: Cesium.PixelDatatype.UNSIGNED_BYTE |
| | | }) |
| | | }), |
| | | viewer.clock.onTick.addEventListener(this.activeVideoListener) |
| | | } |
| | | } |
| | | |
| | | locate () { |
| | | var cameraPosition = Cesium.clone(this.cameraPosition), |
| | | position = Cesium.clone(this.position) |
| | | this.viewer.Camera.position = cameraPosition, |
| | | this.viewer.camera.direction = Cesium.Cartesian3.subtract(position, cameraPosition, new Cesium.Cartesian3(0, 0, 0)), |
| | | this.viewer.camera.up = Cesium.Cartesian3.normalize(cameraPosition, new Cesium.Cartesian3(0, 0, 0)) |
| | | } |
| | | |
| | | update (e) { |
| | | this.viewShadowMap && this.viewer.scene.frameState.shadowMaps.push(this.viewShadowMap) // *重点* 多投影 |
| | | } |
| | | |
| | | _createShadowMap () { |
| | | var e = Cesium.Cartesian3.fromDegrees(this.param.position.x * 1, this.param.position.y * 1, this.param.position.z * 1) |
| | | var t = this.position |
| | | var i = this.viewer.scene |
| | | var a = new Cesium.Camera(i) |
| | | a.position = e |
| | | |
| | | this.customFrustum = new Cesium.PerspectiveFrustum({ |
| | | fov: Cesium.Math.toRadians(this.fov), |
| | | aspectRatio: this.aspectRatio, |
| | | near: this.near, |
| | | far: 250 |
| | | }) |
| | | |
| | | a.frustum = new Cesium.PerspectiveFrustum({ |
| | | fov: Cesium.Math.toRadians(this.fov), |
| | | aspectRatio: this.aspectRatio, |
| | | near: this.near, |
| | | far: 250 |
| | | }) |
| | | |
| | | let curOrientation = {} |
| | | |
| | | const headingPitchRoll = new Cesium.HeadingPitchRoll( |
| | | Cesium.Math.toRadians(this.param.rotation.y, 0), |
| | | Cesium.Math.toRadians(this.param.rotation.x, 0), |
| | | Cesium.Math.toRadians(0), |
| | | ) |
| | | |
| | | curOrientation['heading'] = headingPitchRoll['heading'] |
| | | curOrientation['pitch'] = headingPitchRoll['pitch'] |
| | | curOrientation['roll'] = headingPitchRoll['roll'] |
| | | |
| | | let curDestination = {} |
| | | |
| | | curDestination.destination = e |
| | | curDestination.orientation = curOrientation |
| | | |
| | | a.setView(curDestination) |
| | | |
| | | var n = Cesium.Cartesian3.distance(t, e) |
| | | this.viewDis = n |
| | | |
| | | if (!this.isShadowMap) return |
| | | |
| | | this.viewShadowMap = new Cesium.ShadowMap({ |
| | | lightCamera: a, |
| | | enable: !1, |
| | | isPointLight: !1, |
| | | isSpotLight: !0, |
| | | cascadesEnabled: !1, |
| | | context: i.context, |
| | | pointLightRadius: n, |
| | | }) |
| | | } |
| | | |
| | | _getOrientation () { |
| | | const hpr = Cesium.HeadingPitchRoll.fromDegrees(180 + this.param.rotation.y * 1, 0, 90 - this.param.rotation.x * 1 || 0) |
| | | const orientation = Cesium.Transforms.headingPitchRollQuaternion(this.cameraPosition, hpr) |
| | | |
| | | return this.orientation = orientation, |
| | | orientation |
| | | } |
| | | |
| | | _addCameraFrustum () { |
| | | var e = this |
| | | |
| | | this.cameraFrustum = new Cesium.Primitive({ |
| | | geometryInstances: new Cesium.GeometryInstance({ |
| | | geometry: new Cesium.FrustumGeometry({ |
| | | origin: e.cameraPosition, |
| | | orientation: e.orientation, |
| | | frustum: e.customFrustum, |
| | | _drawNearPlane: !0 |
| | | }), |
| | | attributes: { |
| | | color: Cesium.ColorGeometryInstanceAttribute.fromColor( |
| | | Cesium.Color.fromBytes(0, 213, 144, 20) |
| | | ), |
| | | } |
| | | }), |
| | | releaseGeometryInstances: false, |
| | | appearance: new Cesium.PerInstanceColorAppearance({ |
| | | translucent: true, |
| | | flat: !0, |
| | | closed: true, |
| | | }), |
| | | asynchronous: false, |
| | | show: this.debugFrustum && this.show |
| | | }) |
| | | |
| | | this.cameraFrustumOutline = new Cesium.Primitive({ |
| | | geometryInstances: new Cesium.GeometryInstance({ |
| | | geometry: new Cesium.FrustumOutlineGeometry({ |
| | | origin: e.cameraPosition, |
| | | orientation: e.orientation, |
| | | frustum: e.customFrustum, |
| | | _drawNearPlane: !0 |
| | | }), |
| | | attributes: { |
| | | color: Cesium.ColorGeometryInstanceAttribute.fromColor( |
| | | Cesium.Color.fromBytes(0, 213, 144, 255) |
| | | ), |
| | | } |
| | | }), |
| | | appearance: new Cesium.PerInstanceColorAppearance({ |
| | | closed: true, |
| | | flat: true, |
| | | translucent: true |
| | | }), |
| | | asynchronous: false, |
| | | show: this.debugFrustum && this.show |
| | | }) |
| | | |
| | | this.viewer.scene.primitives.add(this.cameraFrustum) |
| | | this.viewer.scene.primitives.add(this.cameraFrustumOutline) |
| | | } |
| | | |
| | | _addPostProcess () { |
| | | if (!this.isShadowMap) return |
| | | |
| | | var e = this, |
| | | t = videoShed3dShader |
| | | |
| | | const i = e.viewShadowMap['_primitiveBias'] |
| | | |
| | | this.postProcess = new Cesium.PostProcessStage({ |
| | | fragmentShader: t, |
| | | uniforms: { |
| | | ztzf_frustum_videoTexture: function () { |
| | | return e.videoTexture |
| | | }, |
| | | |
| | | ztzf_frustum_maskTexture: function () { |
| | | return new Cesium['Texture']({ |
| | | 'context': e.viewer.scene.context, |
| | | 'source': { |
| | | 'width': 0x1, |
| | | 'height': 0x1, |
| | | 'arrayBufferView': new Uint8Array([255, 255, 255, 255]) |
| | | }, |
| | | 'flipY': ![] |
| | | }) |
| | | }, |
| | | |
| | | ztzf_frustum_opacity: function () { |
| | | return e.alpha |
| | | }, |
| | | |
| | | ztzf_frustum_hiddenAreaColor: () => { |
| | | return new Cesium.Color(0, 0, 0, 0.5) |
| | | }, |
| | | |
| | | shadowMap_texture: function () { |
| | | if (!e.viewShadowMap || !e.viewShadowMap['_shadowMapTexture'] || e.viewShadowMap['_shadowMapTexture']['isDestroyed']()) { |
| | | return new Cesium['Texture']({ |
| | | 'context': e.viewer.scene.context, |
| | | 'source': { |
| | | 'width': 0x1, |
| | | 'height': 0x1, |
| | | 'arrayBufferView': new Uint8Array([0x0, 0x0, 0x0, 0x0]) |
| | | }, |
| | | 'flipY': ![] |
| | | }) |
| | | } |
| | | |
| | | return e.viewShadowMap._shadowMapTexture |
| | | }, |
| | | |
| | | shadowMap_matrix: function () { |
| | | return e.viewShadowMap._shadowMapMatrix |
| | | }, |
| | | |
| | | shadowMap_lightPositionEC: function () { |
| | | return e.viewShadowMap._lightPositionEC |
| | | }, |
| | | |
| | | shadowMap_texelSizeDepthBias: function () { |
| | | var t = new Cesium.Cartesian2 |
| | | return t.x = 1 / e.viewShadowMap._textureSize.x, |
| | | t.y = 1 / e.viewShadowMap._textureSize.y, |
| | | Cesium.Cartesian4.fromElements(t.x, t.y, i.depthBias, i.normalShadingSmooth, this.combinedUniforms1) |
| | | }, |
| | | |
| | | shadowMap_normalOffsetScale: function () { |
| | | return Cesium.Cartesian4.fromElements(i.normalOffsetScale, e.viewShadowMap._distance, e.viewShadowMap.maximumDistance, e.viewShadowMap._darkness, this.combinedUniforms2) |
| | | }, |
| | | |
| | | ztzf_frustum_flipx: () => { |
| | | return false |
| | | }, |
| | | |
| | | ztzf_frustum_flipy: () => { |
| | | return false |
| | | }, |
| | | |
| | | ztzf_frustum_borderColor: () => { |
| | | return new Cesium.Color(0 / 255, 213 / 255, 144 / 255, 255 / 255) |
| | | }, |
| | | |
| | | ztzf_frustum_borderWidth: () => { |
| | | return 0.01 |
| | | }, |
| | | } |
| | | }), |
| | | this.viewer.scene.postProcessStages.add(this.postProcess) |
| | | } |
| | | |
| | | destroy () { |
| | | this.viewer.scene.postProcessStages.remove(this.postProcess), |
| | | this.viewer.scene.primitives.remove(this.cameraFrustum), |
| | | this.viewer.scene.primitives.remove(this.cameraFrustumOutline), |
| | | //this._videoEle && this._videoEle.parentNode.removeChild(this._videoEle), |
| | | this.activeVideoListener && this.viewer.clock.onTick.removeEventListener(this.activeVideoListener), |
| | | this.activeVideoListener && delete this.activeVideoListener, |
| | | delete this.postProcess, |
| | | delete this.viewShadowMap, |
| | | delete this.color, |
| | | delete this.viewDis, |
| | | delete this.cameraPosition, |
| | | delete this.position, |
| | | delete this.alpha, |
| | | delete this._camerafov, |
| | | delete this._isShadowMap, |
| | | delete this._cameraPosition, |
| | | delete this.videoTexture, |
| | | delete this.cameraFrustum, |
| | | delete this.cameraFrustumOutline, |
| | | delete this._videoEle, |
| | | delete this._debugFrustum, |
| | | delete this._position, |
| | | delete this._aspectRatio, |
| | | delete this.element, |
| | | delete this.orientation, |
| | | delete this.texture, |
| | | delete this.videoId, |
| | | this.viewer.scene.primitives.remove(this), |
| | | delete this.viewer |
| | | } |
| | | } |
| | | |
| | | export default CesiumVideoFrustum |
| New file |
| | |
| | | /* |
| | | * @Author : yuan |
| | | * @Date : 2025-08-15 14:06:49 |
| | | * @LastEditors : yuan |
| | | * @LastEditTime : 2025-08-15 14:06:59 |
| | | * @FilePath : \src\utils\cesium\frustum\CoordinateTranslate.js |
| | | * @Description : |
| | | * Copyright 2025 OBKoro1, All Rights Reserved. |
| | | * 2025-08-15 14:06:49 |
| | | */ |
| | | let ECEF = (function () { |
| | | var _ = function () { |
| | | this.PI = 3.141592653589793238 |
| | | this.a = 6378137.0 |
| | | this.b = 6356752.3142 |
| | | this.f = (this.a - this.b) / this.a |
| | | this.e_sq = this.f * (2.0 - this.f) |
| | | this.ee = 0.00669437999013 |
| | | this.WGSF = 1 / 298.257223563 |
| | | this.WGSe2 = this.WGSF * (2 - this.WGSF) |
| | | this.WGSa = 6378137.00000 |
| | | this.EPSILON = 1.0e-12 |
| | | } |
| | | _.prototype.CalculateCoordinates = function (point, azimuth, elevation, distance) { |
| | | var vertical_height = distance * Math.sin(2 * this.PI / 360 * elevation)//垂直高度 |
| | | var horizontal_distance = distance * Math.cos(2 * this.PI / 360 * elevation)//水平距离 |
| | | if (azimuth > 360) azimuth = azimuth % 360 |
| | | if (azimuth < 0) azimuth = 360 + (azimuth % 360) |
| | | |
| | | var point1 = this.lonLat2WebMercator(point) |
| | | var lnglat = null |
| | | |
| | | var x_length, y_length |
| | | if (azimuth <= 90) {//第四象限 |
| | | x_length = horizontal_distance * Math.cos(2 * this.PI / 360 * azimuth) |
| | | y_length = horizontal_distance * Math.sin(2 * this.PI / 360 * azimuth) |
| | | lnglat = { |
| | | x: point1.x + x_length, |
| | | y: point1.y - y_length |
| | | } |
| | | } else if (azimuth > 90 && azimuth <= 180) {//第三象限 |
| | | x_length = horizontal_distance * Math.sin(2 * this.PI / 360 * (azimuth - 90)) |
| | | y_length = horizontal_distance * Math.cos(2 * this.PI / 360 * (azimuth - 90)) |
| | | lnglat = { |
| | | x: point1.x - x_length, |
| | | y: point1.y - y_length |
| | | } |
| | | } else if (azimuth > 180 && azimuth <= 270) {//第二象限 |
| | | x_length = horizontal_distance * Math.cos(2 * this.PI / 360 * (azimuth - 180)) |
| | | y_length = horizontal_distance * Math.sin(2 * this.PI / 360 * (azimuth - 180)) |
| | | lnglat = { |
| | | x: point1.x - x_length, |
| | | y: point1.y + y_length |
| | | } |
| | | } else {//第一象限 |
| | | x_length = horizontal_distance * Math.sin(2 * this.PI / 360 * (azimuth - 270)) |
| | | y_length = horizontal_distance * Math.cos(2 * this.PI / 360 * (azimuth - 270)) |
| | | lnglat = { |
| | | x: point1.x + x_length, |
| | | y: point1.y + y_length |
| | | } |
| | | } |
| | | lnglat = this.webMercator2LonLat(lnglat) |
| | | return { |
| | | lng: lnglat.x, |
| | | lat: lnglat.y, |
| | | height: vertical_height |
| | | } |
| | | } |
| | | /* |
| | | *经纬度转Web墨卡托 |
| | | *@lonLat 经纬度 |
| | | */ |
| | | _.prototype.lonLat2WebMercator = function (lonLat) { |
| | | let x = lonLat.x * this.a / 180 |
| | | let y = Math.log(Math.tan((90 + lonLat.y) * this.PI / 360)) / (this.PI / 180) |
| | | y = y * this.a / 180 |
| | | return { |
| | | x: x, |
| | | y: y |
| | | } |
| | | } |
| | | |
| | | /* |
| | | *Web墨卡托转经纬度 |
| | | *@mercator 平面坐标 |
| | | */ |
| | | _.prototype.webMercator2LonLat = function (mercator) { |
| | | let x = mercator.x / this.a * 180 |
| | | let y = mercator.y / this.a * 180 |
| | | y = 180 / this.PI * (2 * (Math.exp(y * this.PI / 180)) - this.PI / 2) |
| | | return { |
| | | x: x, |
| | | y: y |
| | | } |
| | | } |
| | | |
| | | _.prototype.get_atan = function (z, y) { |
| | | let x |
| | | if (z == 0) { |
| | | x = this.PI / 2 |
| | | } else { |
| | | if (y == 0) { |
| | | x = this.PI |
| | | } else { |
| | | x = Math.atan(Math.abs(y / z)) |
| | | if ((y > 0) && (z < 0)) { |
| | | x = this.PI - x |
| | | } else if ((y < 0) && (z < 0)) { |
| | | x = this.PI + x |
| | | } else if ((y < 0) && (z > 0)) { |
| | | x = 2 * this.M_PI - x |
| | | } |
| | | } |
| | | } |
| | | return x |
| | | } |
| | | //WGS84转ECEF坐标系 |
| | | _.prototype.ConvertLLAToXYZ = function (LLACoor) { |
| | | let lon = this.PI / 180 * LLACoor.longitude |
| | | let lat = this.PI / 180 * LLACoor.latitude |
| | | let H = LLACoor.altitude |
| | | let N0 = this.a / Math.sqrt(1.0 - this.ee * Math.sin(lat) * Math.sin(lat)) |
| | | let x = (N0 + H) * Math.cos(lat) * Math.cos(lon) |
| | | let y = (N0 + H) * Math.cos(lat) * Math.sin(lon) |
| | | let z = (N0 * (1.0 - this.ee) + H) * Math.sin(lat) |
| | | return { |
| | | x: x, |
| | | y: y, |
| | | z: z |
| | | } |
| | | } |
| | | |
| | | //ECEF坐标系转WGS84 |
| | | _.prototype.ConvertXYZToLLA = function (XYZCoor) { |
| | | let longitude = this.get_atan(XYZCoor.x, XYZCoor.y) |
| | | if (longitude < 0) { |
| | | longitude = longitude + this.PI |
| | | } |
| | | let latitude = this.get_atan(Math.sqrt(XYZCoor.x * XYZCoor.x + XYZCoor.y * XYZCoor.y), XYZCoor.z) |
| | | |
| | | let W = Math.sqrt(1 - this.WGSe2 * Math.sin(latitude) * Math.sin(latitude)) |
| | | let N = this.WGSa / W |
| | | let B1 |
| | | do { |
| | | B1 = latitude |
| | | W = Math.sqrt(1 - this.WGSe2 * Math.sin(B1) * Math.sin(B1)) |
| | | N = this.WGSa / W |
| | | latitude = this.get_atan(Math.sqrt(XYZCoor.x * XYZCoor.x + XYZCoor.y * XYZCoor.y), (XYZCoor.z + N * this.WGSe2 * Math.sin(B1))) |
| | | } |
| | | while (Math.abs(latitude - B1) > this.EPSILON) |
| | | |
| | | var altitude = Math.sqrt(XYZCoor.x * XYZCoor.x + XYZCoor.y * XYZCoor.y) / Math.cos(latitude) - this.WGSa / Math.sqrt(1 - this.WGSe2 * Math.sin(latitude) * Math.sin(latitude)) |
| | | |
| | | return { |
| | | longitude: longitude * 180 / this.PI, |
| | | latitude: latitude * 180 / this.PI, |
| | | altitude: altitude |
| | | } |
| | | } |
| | | /*北东天坐标系转WGS84 |
| | | @ a A点坐标 |
| | | @ p 相对参数,距离、方位角、仰角 |
| | | */ |
| | | // 俯视角pitch -elevation |
| | | //航向角heading(yaw) -azimuth |
| | | _.prototype.enu_to_ecef = function (a, p) { |
| | | //距离 |
| | | let distance = p.distance |
| | | //方位角 |
| | | let azimuth = p.azimuth |
| | | //仰角 |
| | | let elevation = p.elevation |
| | | |
| | | let zUp = elevation >= 0 ? distance * Math.sin(this.PI / 180 * elevation) : (-1) * distance * Math.sin(this.PI / 180 * Math.abs(elevation)) |
| | | |
| | | let d = distance * Math.cos(this.PI / 180 * Math.abs(elevation)) |
| | | let xEast |
| | | let yNorth |
| | | if (azimuth <= 90) { |
| | | xEast = d * Math.sin(this.PI / 180 * azimuth) |
| | | yNorth = d * Math.cos(this.PI / 180 * azimuth) |
| | | } else if (azimuth > 90 && azimuth < 180) { |
| | | xEast = d * Math.cos(this.PI / 180 * (azimuth - 90)) |
| | | yNorth = (-1) * d * Math.sin(this.PI / 180 * (azimuth - 90)) |
| | | } else if (azimuth > 180 && azimuth < 270) { |
| | | xEast = (-1) * d * Math.sin(this.PI / 180 * (azimuth - 180)) |
| | | yNorth = (-1) * d * Math.cos(this.PI / 180 * (azimuth - 180)) |
| | | } else { |
| | | xEast = (-1) * d * Math.sin(this.PI / 180 * (360 - azimuth)) |
| | | yNorth = d * Math.cos(this.PI / 180 * (360 - azimuth)) |
| | | } |
| | | |
| | | let lamb = this.radians(a.latitude) |
| | | let phi = this.radians(a.longitude) |
| | | let h0 = a.altitude |
| | | |
| | | let s = Math.sin(lamb) |
| | | let N = this.a / Math.sqrt(1.0 - this.e_sq * s * s) |
| | | |
| | | let sin_lambda = Math.sin(lamb) |
| | | let cos_lambda = Math.cos(lamb) |
| | | |
| | | let sin_phi = Math.sin(phi) |
| | | let cos_phi = Math.cos(phi) |
| | | |
| | | let x0 = (h0 + N) * cos_lambda * cos_phi |
| | | let y0 = (h0 + N) * cos_lambda * sin_phi |
| | | let z0 = (h0 + (1 - this.e_sq) * N) * sin_lambda |
| | | |
| | | let t = cos_lambda * zUp - sin_lambda * yNorth |
| | | |
| | | let zd = sin_lambda * zUp + cos_lambda * yNorth |
| | | let xd = cos_phi * t - sin_phi * xEast |
| | | let yd = sin_phi * t + cos_phi * xEast |
| | | |
| | | return this.ConvertXYZToLLA({ |
| | | x: xd + x0, |
| | | y: yd + y0, |
| | | z: zd + z0 |
| | | }) |
| | | } |
| | | _.prototype.radians = function (degree) { |
| | | return this.PI / 180 * degree |
| | | } |
| | | return _ |
| | | })() |
| | | |
| | | export default ECEF |
| | |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 递归处理对象,移除 #text 键并返回其值,同时处理数值字符串到数字的转换 |
| | | * @param {any} obj - 需要处理的对象或值 |
| | | * @returns {any} 处理后的对象或值 |
| | | */ |
| | | export function removeTextKey(obj) { |
| | | if (typeof obj !== 'object' || obj === null) { |
| | | // 如果是数值字符串,转换为数值 |
| | |
| | | } |
| | | |
| | | if (Array.isArray(obj)) { |
| | | // 递归处理数组中的每个元素 |
| | | return obj.map(item => removeTextKey(item)); |
| | | } |
| | | |
| | | if (Object.prototype.hasOwnProperty.call(obj, '#text')) { |
| | | // 如果 #text 的值是数值字符串,转换为数值 |
| | | // 如果存在 #text 属性,返回其值(如果是数值字符串则转换为数字) |
| | | const textValue = obj['#text']; |
| | | return typeof textValue === 'string' && /^-?\d+(\.\d+)?$/.test(textValue) ? Number(textValue) : textValue; |
| | | } |
| | | |
| | | // 递归处理对象属性 |
| | | const newObj = {}; |
| | | for (const key in obj) { |
| | | if (Object.prototype.hasOwnProperty.call(obj, key)) { |
| | | // 如果属性值是空对象,则设为空字符串;否则递归处理 |
| | | newObj[key] = Object.keys(obj[key]).length === 0 ? '' : removeTextKey(obj[key]); |
| | | } |
| | | } |
| | |
| | | import * as Cesium from 'cesium' |
| | | import * as turf from '@turf/turf' |
| | | const { VITE_APP_BASE, VITE_APP_ENV, VITE_APP_REGION_URL } = import.meta.env |
| | | import store from '@/store' |
| | | |
| | | const { VITE_APP_BASE, VITE_APP_ENV, VITE_APP_REGION_URL } = import.meta.env |
| | | export const getLngLatDistance = (lat1, lng1, lat2, lng2) => { |
| | | const radLat1 = (lat1 * Math.PI) / 180.0 |
| | | const radLat2 = (lat2 * Math.PI) / 180.0 |
| | |
| | | const newArr = arr.sort((a, b) => a.distance - b.distance) |
| | | return newArr[0] |
| | | } |
| | | // 获取多边形内部数据 |
| | | export const samplePolygonInterior = (positions, samplingDistance) => { |
| | | if (!positions.length) return [] |
| | | let minLng = Infinity, |
| | | maxLng = -Infinity |
| | | let minLat = Infinity, |
| | | maxLat = -Infinity |
| | | positions.forEach(pos => { |
| | | minLng = Math.min(minLng, pos.lng) |
| | | maxLng = Math.max(maxLng, pos.lng) |
| | | minLat = Math.min(minLat, pos.lat) |
| | | maxLat = Math.max(maxLat, pos.lat) |
| | | }) |
| | | |
| | | // 将 Cesium 多边形格式转为 Turf 格式 |
| | | const turfPolygonCoords = positions.map(pos => [pos.lng, pos.lat]) |
| | | turfPolygonCoords.push([positions[0].lng, positions[0].lat]) // 闭合多边形 |
| | | const turfPolygon = turf.polygon([turfPolygonCoords]) |
| | | |
| | | // 生成网格点并判断是否在多边形内 |
| | | const sampledPoints = [] |
| | | for (let lng = minLng; lng <= maxLng; lng += samplingDistance) { |
| | | for (let lat = minLat; lat <= maxLat; lat += samplingDistance) { |
| | | // 创建 Turf 点 |
| | | const turfPoint = turf.point([lng, lat]) |
| | | // Turf 原生方法判断点是否在多边形内 |
| | | if (turf.booleanPointInPolygon(turfPoint, turfPolygon)) { |
| | | sampledPoints.push({ |
| | | lng: Number(lng.toFixed(6)), |
| | | lat: Number(lat.toFixed(6)), |
| | | height: 0, |
| | | }) |
| | | } |
| | | } |
| | | } |
| | | |
| | | return sampledPoints |
| | | } |
| | | // 获取多边形面内的最高点(相对地形高度) |
| | | export const getPolygonHighestPoint = async (positions, viewer, samplingDistance = 0.0002) => { |
| | | try { |
| | | const interiorPoints = samplePolygonInterior(positions, samplingDistance, viewer) |
| | | const allPoints = [...positions, ...interiorPoints] |
| | | // 批量获取所有点的地形高度 |
| | | const pointsWithTerrain = await getPointPositionsHeight(allPoints, viewer) |
| | | // console.log('所有点的地形高度', pointsWithTerrain) |
| | | |
| | | // 地形高度(ASL)最高的点 |
| | | const highestTerrainPoint = pointsWithTerrain.reduce( |
| | | (max, curr) => (curr.ASL > max.ASL ? curr : max), |
| | | pointsWithTerrain[0] |
| | | ) |
| | | |
| | | // console.log('地形最高点信息:', highestTerrainPoint) |
| | | return highestTerrainPoint |
| | | } catch (error) { |
| | | console.error('获取地形最高点失败:', error) |
| | | return null |
| | | } |
| | | } |
| | | |
| | | // 获取当前经纬度地形数据 |
| | | export const getLnglatAltitude = (longitude, latitude, viewer) => { |
| | |
| | | return new Promise((resolve, reject) => { |
| | | if (!data || !data.length) { |
| | | resolve([]) |
| | | |
| | | return |
| | | } |
| | | |
| | | // 假设 viewer 已经初始化并且 terrainProvider 是有效的 |
| | | const terrainProvider = viewer?.terrainProvider |
| | | |
| | | // 创建 Cartographic 对象 |
| | | const cartographics = data.map(item => { |
| | | const { lng, lat } = item |
| | | |
| | | return Cesium.Cartographic.fromDegrees(Number(lng), Number(lat)) |
| | | }) |
| | | |
| | | // 获取地形数据的Promise |
| | | const promise = Cesium.sampleTerrainMostDetailed(terrainProvider, cartographics) |
| | | |
| | | // 使用 Cesium.when 处理 Promise |
| | | promise |
| | | .then(function (updatedPositions) { |
| | | // updatedPositions 是一个数组,包含更新后的 Cartographic 对象 |
| | | // 是一个数组,包含更新后的 Cartographic 对象 |
| | | const newPosition = updatedPositions.map((item, index) => { |
| | | const longitude = Cesium.Math.toDegrees(item.longitude) |
| | | const latitude = Cesium.Math.toDegrees(item.latitude) |
| | | |
| | | let pointData = { |
| | | ...data[index], |
| | | longitude, |
| | |
| | | ASL: Number(item.height), |
| | | customHeight: Number(item.height), |
| | | } |
| | | |
| | | if (droneHeight) pointData.TH = Number(item.height) + Number(droneHeight) |
| | | |
| | | return pointData |
| | | }) |
| | | |
| | | resolve(newPosition) |
| | | // 在这里,你可以使用 height 值进行后续操作 |
| | | }) |
| | | .catch(function (error) { |
| | | // console.error('获取高程时发生错误:', error); |
| | | reject(error) |
| | | console.log('获取高程时发生错误:', error) |
| | | resolve(data.map(item => ({ ...item, ASL: 0, customHeight: 0, longitude: item.lng, latitude: item.lat }))) |
| | | }) |
| | | }) |
| | | } |
| | |
| | | if (!Array.isArray(positionsData) || positionsData.length === 0) return |
| | | |
| | | // 如果是一个点,加两个点方便后续生成外包围盒 |
| | | if (positionsData.length === 1){ |
| | | const [lon,lat,height] = positionsData[0] |
| | | positionsData = [[lon+0.001,lat+0.001,height],[lon-0.001,lat-0.001,height],[lon,lat,height]] |
| | | if (positionsData.length === 1) { |
| | | const [lon, lat, height = 0] = positionsData[0] |
| | | positionsData = [ |
| | | [lon + 0.001, lat + 0.001, height], |
| | | [lon - 0.001, lat - 0.001, height], |
| | | [lon, lat, height], |
| | | ] |
| | | } |
| | | const positions = positionsData.map(([lon, lat, height]) => |
| | | const positions = positionsData.map(([lon, lat, height = 0]) => |
| | | Cesium.Cartesian3.fromDegrees(Number(lon), Number(lat), Number(height || 0)) |
| | | ) |
| | | |
| | |
| | | range: boundingSphere.radius * multiple, |
| | | }, |
| | | complete: () => { |
| | | // 飞行完成后检查相机高度是否小于最高点,如果小于直接飞到最高点 |
| | | // 飞行完成后检查相机高度是否小于最高点 |
| | | const cameraHeight = Cesium.Cartographic.fromCartesian(viewer.camera.position).height |
| | | if (cameraHeight < maxHeight) { |
| | | viewer.camera.flyTo({ |
| | |
| | | |
| | | // 辅助函数:创建旋转文字的画布 |
| | | export function createRotatedTextCanvas(time) { |
| | | const prefix = '预计' |
| | | const prefix = '预计到达' |
| | | const suffix = '分钟' |
| | | const canvas = document.createElement('canvas') |
| | | const context = canvas.getContext('2d') |
| | |
| | | context.font = 'bold 16px Source Han Sans CN' |
| | | // 测量各部分文本宽度 |
| | | const prefixWidth = context.measureText(prefix).width |
| | | const timeWidth = context.measureText(time).width |
| | | let timeWidth = context.measureText(time).width |
| | | const timeWidthMore = `${time >= 10 ? 6 : 0}` |
| | | timeWidth = timeWidth + Number(timeWidthMore) |
| | | const suffixWidth = context.measureText(suffix).width |
| | | const totalWidth = prefixWidth + timeWidth + suffixWidth |
| | | const textHeight = 20 // 估算高度 |
| | |
| | | context.textAlign = 'left' // 改为左对齐,方便分段绘制 |
| | | // 清除画布 |
| | | context.clearRect(0, 0, canvas.width, canvas.height) |
| | | // 绘制“预计”(白色) |
| | | // 绘制“预计到达”(白色) |
| | | context.fillStyle = '#FFFFFF' |
| | | context.fillText(prefix, 10, 15) |
| | | // 绘制时间(蓝色) |
| | | context.font = 'bold 30px Source Han Sans CN' |
| | | context.font = 'bold 24px Source Han Sans CN' |
| | | context.fillStyle = '#1EE7E7' // 亮蓝色 |
| | | context.fillText(time, 10 + prefixWidth, 12) |
| | | // 设置宽度 |
| | | context.textBaseline = 'middle' |
| | | context.fillText(time, 10 + prefixWidth, 14) |
| | | // 绘制“分钟”(白色) |
| | | context.font = 'bold 16px Source Han Sans CN' |
| | | context.fillStyle = '#FFFFFF' |
| | | context.fillText(suffix, 20 + prefixWidth + timeWidth, 15) |
| | | context.fillText(suffix, 14 + prefixWidth + timeWidth, 15) |
| | | |
| | | return canvas |
| | | } |
| | | |
| | | |
| | | export const saveCurrentCameraPosition = viewer => { |
| | | const position = viewer?.camera.position.clone() // 相机位置(Cartesian3) |
| | | const heading = viewer?.camera.heading // 航向角(弧度) |
| | | const pitch = viewer?.camera.pitch // 俯仰角(弧度) |
| | | const roll = viewer?.camera.roll // 翻滚角(弧度) |
| | | store.commit('setCameraPosition', { |
| | | duration: 0, |
| | | destination: position, |
| | | orientation: { heading, pitch, roll }, |
| | | }) |
| | | } |
| | | |
| | | // 连接地面线 |
| | | export function getDockPolyLine(data, viewer) { |
| | | if (!data.longitude || !data.latitude) return {} |
| | | return { |
| | | positions: new Cesium.CallbackProperty(() => { |
| | | const pointPosition = Cesium.Cartesian3.fromDegrees(+data.longitude, +data.latitude, +data.height) |
| | | if (!pointPosition) return [] |
| | | // 获取点的位置 |
| | | const cartographic = Cesium.Cartographic.fromCartesian(pointPosition) |
| | | // 获取地形高度 |
| | | const terrainHeight = viewer.scene.globe.getHeight(cartographic) || 0 |
| | | // 创建地面点位置 |
| | | const groundPosition = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, terrainHeight) |
| | | return [pointPosition, groundPosition] |
| | | }, false), |
| | | width: 0.5, |
| | | material: Cesium.Color.WHITE, |
| | | } |
| | | } |
| | | |
| | | // 飞向一个机巢中心 |
| | | export const flyDockCenter = (viewer, info) => { |
| | | const { longitude = 115, latitude = 28, height = 0 } = info |
| | | const position = Cesium.Cartesian3.fromDegrees(+longitude, +latitude, +height) |
| | | const droneEntity = viewer?.entities.add({ position: position, point: { pixelSize: 0 } }) |
| | | viewer?.flyTo(droneEntity, { |
| | | offset: new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-60), 22000), |
| | | duration: 0.5, |
| | | complete: () => { |
| | | viewer?.entities.remove(droneEntity) |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | // 在固定的经度上获得高度 |
| | | export function getHeightOnFixedLonLat(ray, pointParams, ellipsoid = Cesium.Ellipsoid.WGS84) { |
| | | const { longitude: lon, latitude: lat } = pointParams |
| | | // 1. 地表点(高度=0) |
| | | const p0 = Cesium.Cartesian3.fromDegrees(lon, lat, 0, ellipsoid) |
| | | |
| | | // 2. 该点处的法线方向(up 向量,单位向量) |
| | | const up = ellipsoid.geodeticSurfaceNormal(p0, new Cesium.Cartesian3()) |
| | | |
| | | // 3. 射线起点和方向 |
| | | const r0 = ray.origin // 相机位置 |
| | | const rd = ray.direction // 射线方向(已归一化?不一定,但不影响) |
| | | |
| | | // 4. 解:求直线 L(t) = p0 + t * up 与射线 R(s) = r0 + s * rd 的最近点 |
| | | // 使用向量公式求两条直线的最近点参数 t |
| | | const w0 = Cesium.Cartesian3.subtract(p0, r0, new Cesium.Cartesian3()) |
| | | |
| | | const a = Cesium.Cartesian3.dot(up, up) // = 1(因为 up 是单位向量) |
| | | const b = Cesium.Cartesian3.dot(up, rd) |
| | | const c = Cesium.Cartesian3.dot(rd, rd) // = |rd|^2 |
| | | const d = Cesium.Cartesian3.dot(up, w0) |
| | | const e = Cesium.Cartesian3.dot(rd, w0) |
| | | |
| | | const denom = a * c - b * b |
| | | let t |
| | | |
| | | if (Math.abs(denom) < Cesium.Math.EPSILON10) { |
| | | // 两直线平行,取 t = 0 |
| | | t = 0 |
| | | } else { |
| | | t = (b * e - c * d) / denom |
| | | } |
| | | |
| | | // 5. 得到高度线上对应的点 |
| | | const resultPoint = Cesium.Cartesian3.add( |
| | | p0, |
| | | Cesium.Cartesian3.multiplyByScalar(up, t, new Cesium.Cartesian3()), |
| | | new Cesium.Cartesian3() |
| | | ) |
| | | |
| | | // 6. 转回 Cartographic 获取精确高度 |
| | | const carto = Cesium.Cartographic.fromCartesian(resultPoint, ellipsoid) |
| | | return { |
| | | cartesian: resultPoint, |
| | | height: carto.height, |
| | | longitude: Cesium.Math.toDegrees(carto.longitude), |
| | | latitude: Cesium.Math.toDegrees(carto.latitude), |
| | | } |
| | | } |
| | |
| | | * @param {boolean} [options.flyToContour=false] - 是否飞行到轮廓,默认为 false |
| | | * @param {number} [options.multiple=1.4] - 范围倍数 |
| | | * @param {number} [options.dockOptions={}] - 机巢显示 |
| | | * @param {array} [options.terrainLoadCallback] - 地形加载回调 |
| | | * @param {array} [options.boundaryChange] - 边界改变事件 |
| | | * @param {array} [options.boundaryColor] - 边界颜色 |
| | | * @param {array} [options.dockRangeType] - 机巢范围类型 1显示覆盖圆;2显示覆盖圈 |
| | |
| | | dom, |
| | | flatMode = true, |
| | | terrain = false, |
| | | layerMode = 0, |
| | | layerMode = 17, |
| | | contour = true, |
| | | flyToContour = false, |
| | | multiple = 1.4, |
| | | |
| | | dockOptions = {}, |
| | | |
| | | boundaryColor = '#7FFFD4', |
| | | boundaryChange, |
| | | terrainLoadCallback, |
| | | boundaryColor = '#7FFFD4', |
| | | dockRangeType = 1, |
| | | useDockHeight = false |
| | | } = options |
| | |
| | | baseLayer: false, |
| | | fullscreenButton: false, |
| | | } |
| | | |
| | | // 尝试解决出现多个id相同的dom |
| | | const domIdList = document.querySelectorAll(`#${dom}`) |
| | | if (domIdList.length>1) domIdList[1].remove() |
| | | |
| | | this.viewer = new Viewer(dom, viewerOptions) |
| | | this.viewerDom = dom |
| | |
| | | this.viewer?.cesiumWidget.screenSpaceEventHandler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK) // 禁用双击 |
| | | |
| | | if (terrain) { |
| | | // 正则:第一位非0,第二位任意数字,后10位都是0 |
| | | const re = /^[1-9]\d0{10}$/ |
| | | |
| | | let result = null |
| | | |
| | | // 1️⃣先判断 areaCode |
| | | if (re.test(store.state.user.userInfo.detail.areaCode)) { |
| | | result = store.state.user.userInfo.detail.areaCode.slice(0, 6) |
| | | } else { |
| | | // 2️⃣如果不满足,从 ancestors 里找 |
| | | const matchedItems = store.state.user.userInfo.detail.ancestors |
| | | .split(',') |
| | | .filter(item => re.test(item)) // 过滤出符合条件的 |
| | | |
| | | // 取第一个符合条件的前6位(可以改成 last one 看需求) |
| | | if (matchedItems.length > 0) { |
| | | result = matchedItems[0].slice(0, 6) |
| | | } |
| | | } |
| | | |
| | | try { |
| | | Cesium.CesiumTerrainProvider.fromUrl(`${import.meta.env.VITE_APP_TERRAIN_URL}${result}`, { |
| | | requestVertexNormals: true, // 启用地形法线增强立体感 |
| | | requestWaterMask: true, // 启用水体遮罩效果 |
| | | }).then(terrainProvider => { |
| | | this.viewer.terrainProvider = terrainProvider |
| | | }) |
| | | const noTerrainMechanism = ['1962779164650135554'] |
| | | if (noTerrainMechanism.includes(store.state.user.userInfo?.deptId)) { |
| | | // 使用Cesium World Terrain |
| | | Cesium.createWorldTerrainAsync({ |
| | | requestWaterMask: false, // 请求水域效果 |
| | | requestVertexNormals: true // 请求光照和地形法线 |
| | | }).then(terrainProvider => { |
| | | this.viewer.terrainProvider = terrainProvider |
| | | terrainLoadCallback?.() |
| | | }) |
| | | } else { |
| | | /*// 正则:第一位非0,第二位任意数字,后10位都是0 |
| | | const re = /^[1-9]\d0{10}$/ |
| | | |
| | | let result = null |
| | | |
| | | // 1️⃣先判断 areaCode |
| | | if (re.test(store.state.user.userInfo.detail.areaCode)) { |
| | | result = store.state.user.userInfo.detail.areaCode.slice(0, 6) |
| | | } else { |
| | | // 2️⃣如果不满足,从 ancestors 里找 |
| | | const matchedItems = store.state.user.userInfo.detail.ancestors |
| | | .split(',') |
| | | .filter(item => re.test(item)) // 过滤出符合条件的 |
| | | |
| | | // 取第一个符合条件的前6位(可以改成 last one 看需求) |
| | | if (matchedItems.length > 0) { |
| | | result = matchedItems[0].slice(0, 6) |
| | | } |
| | | } |
| | | |
| | | // 使用公司地形 |
| | | Cesium.CesiumTerrainProvider.fromUrl(`${import.meta.env.VITE_APP_TERRAIN_URL}${result}`, { |
| | | requestVertexNormals: true, // 启用地形法线增强立体感 |
| | | requestWaterMask: true, // 启用水体遮罩效果 |
| | | }).then(terrainProvider => { |
| | | this.viewer.terrainProvider = terrainProvider |
| | | terrainLoadCallback?.() |
| | | })*/ |
| | | } |
| | | } catch (error) { |
| | | console.error('地形加载失败:', error) |
| | | } |
| | |
| | | this.switchFlatMode(flatMode) |
| | | this.switchContour(contour) |
| | | flyToContour && this.flyToContour(contour) |
| | | |
| | | if (Cesium.FeatureDetection.supportsImageRenderingPixelated()) { |
| | | let dpr = window.devicePixelRatio |
| | | while (dpr >= 2.0) dpr /= 2.0 // 避免过高缩放导致模糊 |
| | | this.viewer.resolutionScale = dpr // 设置分辨率缩放比例 |
| | | } |
| | | } |
| | | |
| | | getViewer () { |
| | |
| | | |
| | | // 销毁viewer |
| | | viewerDestroy (removeDom = true) { |
| | | if (!this.viewer) return |
| | | // // 👇 关键:先停止所有动态行为 |
| | | // this.viewer.clock.shouldAnimate = false; |
| | | // this.viewer.scene.requestRenderMode = false; |
| | | // // 取消所有可能的 pending 渲染 |
| | | // if (this.viewer.cesiumWidget) { |
| | | // this.viewer.cesiumWidget.render = () => {}; // 空函数覆盖 |
| | | // } |
| | | // // 清理实体和图层 |
| | | // this.viewer?.entities.removeAll(); |
| | | // this.viewer?.imageryLayers.removeAll(); |
| | | // // 清理数据源 |
| | | // try { |
| | | // const dataSources = this.viewer.dataSources; |
| | | // while (dataSources.length > 0) { |
| | | // dataSources.remove(dataSources.get(0)); |
| | | // } |
| | | // } catch (e) { |
| | | // console.warn('清理数据源时出错:', e); |
| | | // } |
| | | // // 清理场景中的图元 |
| | | // try { |
| | | // this.viewer?.scene.primitives.removeAll(); |
| | | // } catch (e) { |
| | | // console.warn('清理图元时出错:', e); |
| | | // } |
| | | // // 获取webgl上下文 |
| | | // let gl = this.viewer?.scene.context._originalGLContext |
| | | // if (gl) { |
| | | // try { |
| | | // gl.canvas.width = 1 |
| | | // gl.canvas.height = 1 |
| | | // gl.getExtension("WEBGL_lose_context").loseContext() |
| | | // } catch (e) { |
| | | // console.warn('释放WebGL上下文时出错:', e) |
| | | // } |
| | | // gl = null |
| | | // } |
| | | this.viewer?.destroy() // 销毁Viewer实例 |
| | | this.viewer = null |
| | | smallMpaCenterDistance = null |
| | | |
| | | if (this.viewer) { |
| | | this.viewer?.entities.removeAll() |
| | | this.viewer?.imageryLayers.removeAll() |
| | | this.viewer?.dataSources.removeAll() |
| | | |
| | | |
| | | // try { |
| | | // this.viewer?.scene?.primitives?.removeAll() |
| | | // this.viewer?.entities?.removeAll() |
| | | // } catch (e) { |
| | | // console.warn('清理资源时出错:', e) |
| | | |
| | | // console.log(this.viewer, 1111111111111) |
| | | // } |
| | | |
| | | // 获取webgl上下文 |
| | | let gl = this.viewer?.scene.context._originalGLContext |
| | | gl.canvas.width = 1 |
| | | gl.canvas.height = 1 |
| | | gl.getExtension("WEBGL_lose_context").loseContext() |
| | | gl = null |
| | | this.viewer?.destroy() // 销毁Viewer实例 |
| | | this.viewer = null |
| | | var cesiumContainer = document.getElementById(this.viewerDom) |
| | | if (cesiumContainer && removeDom) { |
| | | cesiumContainer.remove() // 移除与地图相关的DOM元素 |
| | | this.viewerDom = null |
| | | } |
| | | var cesiumContainer = document.getElementById(this.viewerDom) |
| | | if (cesiumContainer && removeDom) { |
| | | cesiumContainer.remove() // 移除与地图相关的DOM元素 |
| | | this.viewerDom = null |
| | | } |
| | | } |
| | | |
| | | setShowDock (sns) { |
| | | this.boundary?.setShowDock(sns) |
| | | } |
| | | |
| | | setDockCoverColor (sns) { |
| | | this.boundary?.setDockCoverColor(sns) |
| | | } |
| | | // 切换轮廓显示 |
| | | async switchContour (open) { |
| | | if (open) { |
| | | await this.boundary?.openContour() |
| | | |
| | | } else { |
| | | this.boundary?.closeContour() |
| | | } |
| | | } |
| | | // // 飞向轮廓居中 |
| | | // 飞向轮廓居中 |
| | | flyToContour () { |
| | | this.boundary?.flyToBoundary() |
| | | } |
| | |
| | | this.globalBaseMapLayers?.forEach(item => { |
| | | if (item.mapLayer) item.mapLayer.show = false |
| | | }) |
| | | // store.state.common.mapSetting.mode = mode |
| | | store.state.common.mapSetting.mode = mode |
| | | // 标准地图(天地图矢量)加载 |
| | | if (mode === 0) { |
| | | mapLayers.push( |
| | |
| | | this.viewer?.imageryLayers.lowerToBottom(find.mapLayer) |
| | | }) |
| | | } |
| | | |
| | | // 设置或重置滤镜 |
| | | |
| | | setCurrentLayerFilter (data, mode) { |
| | | if (mode === 17) { |
| | | data.mapLayer.brightness = 0.6 // 亮度(默认值) |
| New file |
| | |
| | | import _ from 'lodash' |
| | | import JSZIP from 'jszip' |
| | | import { JSONToXML } from './kmz' |
| | | import { getDateFromTimestamp } from '@/utils/date' |
| | | |
| | | export default function kmzFile () { |
| | | // 云台信息 |
| | | const droneModel = { |
| | | 52: '0', |
| | | 53: '1', |
| | | 66: '0', |
| | | 67: '1', |
| | | 80: '0', |
| | | 81: '1', |
| | | } |
| | | // 创建图斑航线 |
| | | const create = async (waylineBasicInfo, lnglats, spotList) => { |
| | | try { |
| | | const date = new Date() |
| | | const username = window.localStorage.getItem('bs_username') |
| | | const updateTime = date.getTime() |
| | | const fileName = '图斑航线规划_' + date.getTime() |
| | | // 无人机机型、云台型号、航线高度 |
| | | const { droneEnumValue, payloadEnumValue, height } = waylineBasicInfo |
| | | const defaultHeight = height || 100 |
| | | // 起始点位 (参考点位-具体以机场为准) |
| | | const startPoint = lnglats[0] |
| | | const takeOffPoint = [startPoint[1], startPoint[0], 0].join(',') |
| | | // template.xml对象模版 |
| | | const fileTemplate = { |
| | | author: { '#text': username }, |
| | | createTime: { '#text': updateTime }, |
| | | updateTime: { '#text': updateTime }, |
| | | missionConfig: { |
| | | // 飞向首航点模式 |
| | | flyToWaylineMode: { '#text': 'safely' }, |
| | | // 航线结束动作 |
| | | finishAction: { '#text': 'goHome' }, |
| | | // 失控是否继续执行航线 |
| | | exitOnRCLost: { '#text': 'goContinue' }, |
| | | // 失控动作类型 |
| | | executeRCLostAction: { '#text': 'goBack' }, |
| | | // 安全起飞高度 |
| | | takeOffSecurityHeight: { '#text': 20 }, |
| | | // 参考起飞点 |
| | | takeOffRefPoint: { '#text': takeOffPoint }, |
| | | // 参考起飞点绝对高度 |
| | | takeOffRefPointAGLHeight: { '#text': 0 }, |
| | | // 全局航线过渡速度 |
| | | globalTransitionalSpeed: { '#text': 10 }, |
| | | // 全局返航高度 |
| | | globalRTHHeight: { '#text': defaultHeight }, |
| | | // 无人机参数 |
| | | droneInfo: { |
| | | droneEnumValue: { '#text': droneEnumValue }, |
| | | droneSubEnumValue: { |
| | | '#text': droneModel[payloadEnumValue], |
| | | }, |
| | | }, |
| | | // 云台参数 |
| | | payloadInfo: { |
| | | payloadEnumValue: { '#text': payloadEnumValue }, |
| | | payloadSubEnumValue: { |
| | | '#text': droneModel[payloadEnumValue], |
| | | }, |
| | | payloadPositionIndex: { '#text': 0 }, |
| | | }, |
| | | }, |
| | | Folder: { |
| | | // 预定义模板类型 - 航点飞行 |
| | | templateType: { '#text': 'waypoint' }, |
| | | // 是否使用全局航线过渡速度 |
| | | useGlobalTransitionalSpeed: { '#text': 0 }, |
| | | templateId: { '#text': 0 }, |
| | | // 坐标系参数信息 |
| | | waylineCoordinateSysParam: { |
| | | // 经纬度坐标系 - 固定值 |
| | | coordinateMode: { '#text': 'WGS84' }, |
| | | // 航点高程参考平面 - 相对高度 |
| | | heightMode: { '#text': 'relativeToStartPoint' }, |
| | | }, |
| | | // 全局航线飞行速度 |
| | | autoFlightSpeed: { '#text': 10 }, |
| | | // 云台俯仰角控制模式 |
| | | gimbalPitchMode: { '#text': 'usePointSetting' }, |
| | | // 全局高度 |
| | | globalHeight: { '#text': '100' }, |
| | | // 全局偏航角模式参数 - 可使用当前默认值 |
| | | globalWaypointHeadingParam: { |
| | | waypointHeadingMode: { '#text': 'followWayline' }, |
| | | waypointHeadingAngle: { '#text': 0 }, |
| | | waypointPoiPoint: { |
| | | '#text': '0.000000,0.000000,0.000000', |
| | | }, |
| | | waypointHeadingPathMode: { '#text': 'followBadArc' }, |
| | | waypointHeadingPoiIndex: { '#text': 0 }, |
| | | }, |
| | | // 全局航点类型 - 直线飞行,飞行器到点停 |
| | | globalWaypointTurnMode: { |
| | | '#text': 'toPointAndStopWithDiscontinuityCurvature', |
| | | }, |
| | | // 全局航段轨迹是否尽量贴合直线 |
| | | globalUseStraightLine: { '#text': 1 }, |
| | | // 航点点位信息 - 位置、事件等 |
| | | Placemark: [], |
| | | // payloadParam: { |
| | | // payloadPositionIndex: { '#text': 0 }, |
| | | // focusMode: { '#text': 'firstPoint' }, |
| | | // meteringMode: { '#text': 'average' }, |
| | | // returnMode: { '#text': 'singleReturnFirst' }, |
| | | // samplingRate: { '#text': '240000' }, |
| | | // scanningMode: { '#text': 'repetitive' }, |
| | | // imageFormat: { '#text': 'wide,zoom' }, |
| | | // }, |
| | | }, |
| | | } |
| | | // 航点创建 |
| | | const polygonNo = { |
| | | no: '', |
| | | index: 0, |
| | | } |
| | | lnglats.forEach((lnglat, index) => { |
| | | const obj = spotList.find(spot => spot.dkfw.find(item => _.isEqual(item, lnglat))) |
| | | let fileSuffix = '' |
| | | if (obj) { |
| | | if (polygonNo.no === obj.dkbh) { |
| | | polygonNo.index++ |
| | | } else { |
| | | polygonNo.no = obj.dkbh |
| | | polygonNo.index = 1 |
| | | } |
| | | fileSuffix = `图斑航线拍摄(${obj.dkbh}-${polygonNo.index})` |
| | | } else { |
| | | fileSuffix = '图斑航线拍摄(起点)' |
| | | } |
| | | const placemark = { |
| | | Point: { |
| | | // 航点经纬度 |
| | | coordinates: { '#text': `${lnglat[0]},${lnglat[1]}` }, |
| | | }, |
| | | // 航点index |
| | | index: { '#text': index }, |
| | | // 全局航线高度(椭球高) |
| | | ellipsoidHeight: { '#text': defaultHeight }, |
| | | // 全局航线高度 (EGM96海拔高/相对起飞点高度/AGL相对地面高度) |
| | | height: { '#text': defaultHeight }, |
| | | // 航点飞行速度 |
| | | waypointSpeed: { '#text': 10 }, |
| | | // 沿航线方向 - 可使用当前默认值 |
| | | waypointHeadingParam: { |
| | | waypointHeadingMode: { '#text': 'followWayline' }, |
| | | waypointHeadingAngle: { '#text': 0 }, |
| | | waypointPoiPoint: { |
| | | '#text': '0.000000,0.000000,0.000000', |
| | | }, |
| | | waypointHeadingPathMode: { '#text': 'followBadArc' }, |
| | | waypointHeadingPoiIndex: { '#text': 0 }, |
| | | }, |
| | | // 航点类型 |
| | | waypointTurnParam: { |
| | | waypointTurnMode: { |
| | | '#text': 'toPointAndStopWithDiscontinuityCurvature', |
| | | }, |
| | | waypointTurnDampingDist: { '#text': 0.2 }, |
| | | }, |
| | | // 是否全局参数 |
| | | useGlobalHeight: { '#text': 1 }, |
| | | useGlobalSpeed: { '#text': 1 }, |
| | | useGlobalHeadingParam: { '#text': 1 }, |
| | | useGlobalTurnParam: { '#text': 1 }, |
| | | useStraightLine: { '#text': 1 }, |
| | | // 事件组 |
| | | actionGroup: { |
| | | actionGroupId: { '#text': index }, |
| | | actionGroupStartIndex: { '#text': index }, |
| | | actionGroupEndIndex: { '#text': index }, |
| | | actionGroupMode: { '#text': 'sequence' }, |
| | | actionTrigger: { |
| | | actionTriggerType: { '#text': 'reachPoint' }, |
| | | }, |
| | | // 单个事件 - 如果是多个事件action可改成数组 action: [{}] |
| | | action: { |
| | | actionId: { '#text': 0 }, |
| | | actionActuatorFunc: { '#text': 'takePhoto' }, |
| | | actionActuatorFuncParam: { |
| | | fileSuffix: { '#text': fileSuffix }, |
| | | payloadPositionIndex: { '#text': '0' }, |
| | | useGlobalPayloadLensIndex: { '#text': '1' }, |
| | | }, |
| | | }, |
| | | }, |
| | | } |
| | | fileTemplate.Folder.Placemark.push(placemark) |
| | | }) |
| | | const fileWaylines = waylinesContext(fileTemplate) |
| | | |
| | | return { |
| | | fileName, |
| | | template: fileTemplate, |
| | | waylines: fileWaylines, |
| | | } |
| | | } catch (error) { |
| | | console.log(error) |
| | | return false |
| | | } |
| | | } |
| | | |
| | | // 创建正常航线 |
| | | const createNoramlWaylines = (waylineBasicInfo, lnglats) => { |
| | | try { |
| | | const date = new Date() |
| | | const username = window.localStorage.getItem('bs_username') |
| | | const updateTime = date.getTime() |
| | | const { year, month, day, hours, minutes } = getDateFromTimestamp(updateTime) |
| | | const fileName = '新建航线_' + year + month + day + hours + minutes |
| | | // 无人机机型、云台型号、航线高度 |
| | | const { droneEnumValue, payloadEnumValue, height, waylineName } = waylineBasicInfo |
| | | const defaultHeight = height || 100 |
| | | // 起始点位 (参考点位-具体以机场为准) |
| | | const startPoint = lnglats[0] |
| | | const takeOffPoint = [startPoint.latitude, startPoint.longitude, startPoint.height].join(',') |
| | | // template.xml对象模版 |
| | | const fileTemplate = { |
| | | author: { '#text': username }, |
| | | createTime: { '#text': updateTime }, |
| | | updateTime: { '#text': updateTime }, |
| | | missionConfig: { |
| | | // 飞向首航点模式 |
| | | flyToWaylineMode: { '#text': 'safely' }, |
| | | // 航线结束动作 |
| | | finishAction: { '#text': 'goHome' }, |
| | | // 失控是否继续执行航线 |
| | | exitOnRCLost: { '#text': 'goContinue' }, |
| | | // 失控动作类型 |
| | | executeRCLostAction: { '#text': 'goBack' }, |
| | | // 安全起飞高度 |
| | | takeOffSecurityHeight: { '#text': 20 }, |
| | | // 参考起飞点 |
| | | takeOffRefPoint: { '#text': takeOffPoint }, |
| | | // 参考起飞点绝对高度 |
| | | takeOffRefPointAGLHeight: { '#text': 0 }, |
| | | // 全局航线过渡速度 |
| | | globalTransitionalSpeed: { '#text': 10 }, |
| | | // 全局返航高度 |
| | | globalRTHHeight: { '#text': defaultHeight }, |
| | | // 无人机参数 |
| | | droneInfo: { |
| | | droneEnumValue: { '#text': droneEnumValue }, |
| | | droneSubEnumValue: { |
| | | '#text': droneModel[payloadEnumValue], |
| | | }, |
| | | }, |
| | | // 是否自动绕行(暂未验证M30T机型使用此参数后会不会无法起飞) |
| | | autoRerouteInfo: { |
| | | missionAutoRerouteMode: { '#text': 1 }, |
| | | transitionalAutoRerouteMode: { '#text': 1 }, |
| | | }, |
| | | // 云台参数 |
| | | payloadInfo: { |
| | | payloadEnumValue: { '#text': payloadEnumValue }, |
| | | payloadSubEnumValue: { |
| | | '#text': droneModel[payloadEnumValue], |
| | | }, |
| | | payloadPositionIndex: { '#text': 0 }, |
| | | }, |
| | | }, |
| | | Folder: { |
| | | // 预定义模板类型 - 航点飞行 |
| | | templateType: { '#text': 'waypoint' }, |
| | | // 是否使用全局航线过渡速度 |
| | | useGlobalTransitionalSpeed: { '#text': 0 }, |
| | | templateId: { '#text': 0 }, |
| | | // 坐标系参数信息 |
| | | waylineCoordinateSysParam: { |
| | | // 经纬度坐标系 - 固定值 |
| | | coordinateMode: { '#text': 'WGS84' }, |
| | | // 航点高程参考平面 - 相对高度 |
| | | heightMode: { '#text': 'relativeToStartPoint' }, |
| | | }, |
| | | // 全局航线飞行速度 |
| | | autoFlightSpeed: { '#text': 10 }, |
| | | // 云台俯仰角控制模式 |
| | | gimbalPitchMode: { '#text': 'usePointSetting' }, |
| | | // 全局高度 |
| | | globalHeight: { '#text': '100' }, |
| | | // 全局偏航角模式参数 - 可使用当前默认值 |
| | | globalWaypointHeadingParam: { |
| | | waypointHeadingMode: { '#text': 'followWayline' }, |
| | | waypointHeadingAngle: { '#text': 0 }, |
| | | waypointPoiPoint: { |
| | | '#text': '0.000000,0.000000,0.000000', |
| | | }, |
| | | waypointHeadingPathMode: { '#text': 'followBadArc' }, |
| | | waypointHeadingPoiIndex: { '#text': 0 }, |
| | | }, |
| | | // 全局航点类型 - 直线飞行,飞行器到点停 |
| | | globalWaypointTurnMode: { |
| | | '#text': 'toPointAndStopWithDiscontinuityCurvature', |
| | | }, |
| | | // 全局航段轨迹是否尽量贴合直线 |
| | | globalUseStraightLine: { '#text': 1 }, |
| | | // 航点点位信息 - 位置、事件等 |
| | | Placemark: [], |
| | | payloadParam: { |
| | | payloadPositionIndex: { '#text': 0 }, |
| | | focusMode: { '#text': 'firstPoint' }, |
| | | meteringMode: { '#text': 'average' }, |
| | | returnMode: { '#text': 'singleReturnFirst' }, |
| | | samplingRate: { '#text': '240000' }, |
| | | scanningMode: { '#text': 'repetitive' }, |
| | | imageFormat: { '#text': 'wide,zoom' }, |
| | | }, |
| | | }, |
| | | } |
| | | lnglats.forEach((lnglat, index) => { |
| | | const placemark = { |
| | | Point: { |
| | | // 航点经纬度 |
| | | coordinates: { |
| | | '#text': `${lnglat.longitude},${lnglat.latitude}`, |
| | | }, |
| | | }, |
| | | // 航点index |
| | | index: { '#text': index }, |
| | | // 全局航线高度(椭球高) |
| | | ellipsoidHeight: { '#text': lnglat.height }, |
| | | // 全局航线高度 (EGM96海拔高/相对起飞点高度/AGL相对地面高度) |
| | | height: { '#text': lnglat.height }, |
| | | // 航点飞行速度 |
| | | waypointSpeed: { '#text': lnglat?.speed || 10 }, |
| | | // 沿航线方向 - 可使用当前默认值 |
| | | waypointHeadingParam: { |
| | | waypointHeadingMode: { '#text': 'followWayline' }, |
| | | waypointHeadingAngle: { '#text': 0 }, |
| | | waypointPoiPoint: { |
| | | '#text': '0.000000,0.000000,0.000000', |
| | | }, |
| | | waypointHeadingPathMode: { '#text': 'followBadArc' }, |
| | | waypointHeadingPoiIndex: { '#text': 0 }, |
| | | }, |
| | | // 航点类型 |
| | | waypointTurnParam: { |
| | | waypointTurnMode: { |
| | | '#text': 'toPointAndStopWithDiscontinuityCurvature', |
| | | }, |
| | | waypointTurnDampingDist: { '#text': 0.2 }, |
| | | }, |
| | | // 是否全局参数 |
| | | useGlobalHeight: { '#text': 1 }, |
| | | useGlobalSpeed: { '#text': 0 }, |
| | | useGlobalHeadingParam: { '#text': 1 }, |
| | | useGlobalTurnParam: { '#text': 1 }, |
| | | useStraightLine: { '#text': 1 }, |
| | | } |
| | | if (lnglat?.actions) { |
| | | const actionGroup = { |
| | | actionGroupId: { '#text': index }, |
| | | actionGroupStartIndex: { '#text': index }, |
| | | actionGroupEndIndex: { '#text': index }, |
| | | actionGroupMode: { '#text': 'sequence' }, |
| | | actionTrigger: { |
| | | actionTriggerType: { '#text': 'reachPoint' }, |
| | | }, |
| | | // 单个事件 - 如果是多个事件action可改成数组 action: [{}] |
| | | // action: [{ |
| | | // actionId: { '#text': 0 }, |
| | | // actionActuatorFunc: { '#text': '' }, |
| | | // actionActuatorFuncParam: {}, |
| | | // }], |
| | | action: [], |
| | | } |
| | | lnglat?.actions.forEach((action, index) => { |
| | | const singleAction = { |
| | | actionId: { '#text': index }, |
| | | actionActuatorFunc: { '#text': '' }, |
| | | actionActuatorFuncParam: {}, |
| | | } |
| | | // actionGroup.action['actionActuatorFunc'] = { '#text': action.key } |
| | | // action.params.forEach(p => { |
| | | // if (p.value || p.value !== '') { |
| | | // actionGroup.action['actionActuatorFuncParam'][p.key] = { '#text': p.value } |
| | | // } |
| | | // }) |
| | | singleAction['actionActuatorFunc'] = { |
| | | '#text': action.key, |
| | | } |
| | | action.params.forEach(p => { |
| | | if (p.value || p.value !== '') { |
| | | singleAction['actionActuatorFuncParam'][p.key] = { |
| | | '#text': p.value, |
| | | } |
| | | } |
| | | }) |
| | | if (lnglat?.actions.length > 1) { |
| | | actionGroup.action.push(singleAction) |
| | | } else { |
| | | actionGroup.action = singleAction |
| | | } |
| | | }) |
| | | placemark.actionGroup = actionGroup |
| | | } |
| | | fileTemplate.Folder.Placemark.push(placemark) |
| | | }) |
| | | const fileWaylines = waylinesContext(fileTemplate) |
| | | return { |
| | | fileName: waylineName || fileName, |
| | | template: fileTemplate, |
| | | waylines: fileWaylines, |
| | | } |
| | | } catch (error) { |
| | | console.log(error) |
| | | return error |
| | | } |
| | | } |
| | | |
| | | // 创建面状航线 |
| | | const createPlanarWayline = (waylineBasicInfo, lnglats) => { } |
| | | |
| | | // 创建点状航线 |
| | | const createPointWayLines = (waylineBasicInfo, points, settingInfo, dockPosition) => { |
| | | try { |
| | | const { droneEnumValue, payloadEnumValue, waylineName } = waylineBasicInfo |
| | | |
| | | const { |
| | | imageFormat, |
| | | heightMode, |
| | | globalHeight, |
| | | autoFlightSpeed, |
| | | gimbalPitchMode, |
| | | waypointTurnMode, |
| | | waypointHeadingMode, |
| | | finishAction, |
| | | globalTransitionalSpeed, |
| | | } = settingInfo |
| | | |
| | | const date = new Date() |
| | | const username = window.localStorage.getItem('bs_username') |
| | | const updateTime = date.getTime() |
| | | const { year, month, day, hours, minutes } = getDateFromTimestamp(updateTime) |
| | | const fileName = '新建航线_' + year + month + day + hours + minutes |
| | | // 无人机机型、云台型号、航线高度 |
| | | |
| | | const defaultHeight = 100 |
| | | // 起始点位 (参考点位-具体以机场为准) |
| | | const takeOffPoint = [dockPosition.latitude, dockPosition.longitude, 0].join(',') |
| | | |
| | | // template.xml对象模版 |
| | | const fileTemplate = { |
| | | author: { '#text': username }, |
| | | createTime: { '#text': updateTime }, |
| | | updateTime: { '#text': updateTime }, |
| | | missionConfig: { |
| | | // 飞向首航点模式 |
| | | flyToWaylineMode: { '#text': 'safely' }, |
| | | // 航线结束动作 |
| | | finishAction: { '#text': finishAction || 'goHome' }, |
| | | // 失控是否继续执行航线 |
| | | exitOnRCLost: { '#text': 'executeLostAction' }, |
| | | // 失控动作类型 |
| | | executeRCLostAction: { '#text': 'goBack' }, |
| | | // 安全起飞高度 |
| | | takeOffSecurityHeight: { '#text': 120 }, |
| | | // 参考起飞点 |
| | | takeOffRefPoint: { '#text': takeOffPoint }, |
| | | // 参考起飞点绝对高度 |
| | | takeOffRefPointAGLHeight: { '#text': 0 }, |
| | | // 全局航线过渡速度 |
| | | globalTransitionalSpeed: { '#text': globalTransitionalSpeed || 10 }, |
| | | // 全局返航高度 |
| | | globalRTHHeight: { '#text': defaultHeight }, |
| | | // 无人机参数 |
| | | droneInfo: { |
| | | droneEnumValue: { '#text': droneEnumValue }, |
| | | droneSubEnumValue: { |
| | | '#text': droneModel[payloadEnumValue], |
| | | }, |
| | | }, |
| | | // 是否自动绕行(暂未验证M30T机型使用此参数后会不会无法起飞) |
| | | autoRerouteInfo: { |
| | | missionAutoRerouteMode: { '#text': 1 }, |
| | | transitionalAutoRerouteMode: { '#text': 1 }, |
| | | }, |
| | | // 云台参数 |
| | | payloadInfo: { |
| | | payloadEnumValue: { '#text': payloadEnumValue }, |
| | | payloadSubEnumValue: { |
| | | '#text': droneModel[payloadEnumValue], |
| | | }, |
| | | payloadPositionIndex: { '#text': 0 }, |
| | | }, |
| | | }, |
| | | Folder: { |
| | | // 预定义模板类型 - 航点飞行 |
| | | templateType: { '#text': 'waypoint' }, |
| | | // 是否使用全局航线过渡速度 |
| | | useGlobalTransitionalSpeed: { '#text': 0 }, |
| | | templateId: { '#text': 0 }, |
| | | // 坐标系参数信息 |
| | | waylineCoordinateSysParam: { |
| | | // 经纬度坐标系 - 固定值 |
| | | coordinateMode: { '#text': 'WGS84' }, |
| | | // 航点高程参考平面 - 相对高度 |
| | | heightMode: heightMode, |
| | | }, |
| | | // 全局航线飞行速度 |
| | | autoFlightSpeed: { '#text': autoFlightSpeed || 10 }, |
| | | // 云台俯仰角控制模式 |
| | | gimbalPitchMode: { '#text': gimbalPitchMode || 'manual' }, |
| | | // 全局高度 |
| | | globalHeight: { '#text': globalHeight || '100' }, |
| | | // 全局偏航角模式参数 - 可使用当前默认值 |
| | | globalWaypointHeadingParam: { |
| | | waypointHeadingMode: { '#text': waypointHeadingMode || 'followWayline' }, |
| | | waypointHeadingAngle: { '#text': 0 }, |
| | | waypointPoiPoint: { |
| | | '#text': '0.000000,0.000000,0.000000', |
| | | }, |
| | | waypointHeadingPathMode: { '#text': 'followBadArc' }, |
| | | waypointHeadingPoiIndex: { '#text': 0 }, |
| | | }, |
| | | // 全局航点类型 - 直线飞行,飞行器到点停 |
| | | globalWaypointTurnMode: { |
| | | '#text': waypointTurnMode || 'toPointAndStopWithDiscontinuityCurvature', |
| | | }, |
| | | // 全局航段轨迹是否尽量贴合直线 |
| | | globalUseStraightLine: { '#text': 1 }, |
| | | // 航点点位信息 - 位置、事件等 |
| | | Placemark: [], |
| | | payloadParam: { |
| | | payloadPositionIndex: { '#text': 0 }, |
| | | focusMode: { '#text': 'firstPoint' }, |
| | | meteringMode: { '#text': 'average' }, |
| | | returnMode: { '#text': 'singleReturnFirst' }, |
| | | samplingRate: { '#text': '240000' }, |
| | | scanningMode: { '#text': 'repetitive' }, |
| | | imageFormat: imageFormat, |
| | | }, |
| | | }, |
| | | } |
| | | points.length && |
| | | points.forEach((detail, ind) => { |
| | | const { Point, index, height, ellipsoidHeight, actionGroup, waypointSpeed } = detail |
| | | |
| | | const placemark = { |
| | | Point, |
| | | // 航点index |
| | | index, |
| | | // 全局航线高度(椭球高) |
| | | ellipsoidHeight, |
| | | // 全局航线高度 (EGM96海拔高/相对起飞点高度/AGL相对地面高度) |
| | | height, |
| | | // 航点飞行速度 |
| | | waypointSpeed: { '#text': waypointSpeed || 10 }, |
| | | // 沿航线方向 - 可使用当前默认值 |
| | | waypointHeadingParam: { |
| | | waypointHeadingMode: { |
| | | '#text': waypointHeadingMode || 'followWayline', |
| | | }, |
| | | waypointHeadingAngle: { '#text': 0 }, |
| | | waypointPoiPoint: { |
| | | '#text': '0.000000,0.000000,0.000000', |
| | | }, |
| | | waypointHeadingPathMode: { '#text': 'followBadArc' }, |
| | | waypointHeadingPoiIndex: { '#text': 0 }, |
| | | }, |
| | | // 航点类型 |
| | | waypointTurnParam: { |
| | | waypointTurnMode: { |
| | | '#text': waypointTurnMode || 'toPointAndStopWithDiscontinuityCurvature', |
| | | }, |
| | | waypointTurnDampingDist: { '#text': 0.2 }, |
| | | }, |
| | | // 是否全局参数 |
| | | useGlobalHeight: { '#text': 1 }, |
| | | useGlobalSpeed: { '#text': 0 }, |
| | | useGlobalHeadingParam: { '#text': 1 }, |
| | | useGlobalTurnParam: { '#text': 1 }, |
| | | useStraightLine: { '#text': 1 }, |
| | | } |
| | | if (actionGroup) { |
| | | placemark.actionGroup = actionGroup |
| | | } |
| | | |
| | | fileTemplate.Folder.Placemark.push(placemark) |
| | | }) |
| | | |
| | | const fileWaylines = waylinesContext(fileTemplate) |
| | | return { |
| | | fileName: waylineName || fileName, |
| | | template: fileTemplate, |
| | | waylines: fileWaylines, |
| | | } |
| | | } catch (error) { |
| | | console.log(error) |
| | | return error |
| | | } |
| | | } |
| | | |
| | | const save = async fileInfo => { |
| | | const JsZip = new JSZIP() |
| | | const { fileName, template, waylines } = fileInfo |
| | | const templateXml = JSONToXML(template, '', true) |
| | | const waylinesWpml = JSONToXML(waylines, '', true) |
| | | JsZip.file('wpmz/template.kml', templateXml) |
| | | JsZip.file('wpmz/waylines.wpml', waylinesWpml) |
| | | const fileBlob = await JsZip.generateAsync({ type: 'blob' }) |
| | | // saveAs(kmzFile, `${fileName}.kmz`) |
| | | return fileBlob |
| | | } |
| | | |
| | | const waylinesContext = templateJson => { |
| | | const waylinesObj = { |
| | | // 标准模版 |
| | | missionConfig: { |
| | | flyToWaylineMode: null, |
| | | finishAction: null, |
| | | exitOnRCLost: null, |
| | | executeRCLostAction: null, |
| | | takeOffSecurityHeight: null, |
| | | globalTransitionalSpeed: null, |
| | | globalRTHHeight: null, |
| | | droneInfo: null, |
| | | payloadInfo: null, |
| | | }, |
| | | Folder: { |
| | | templateId: null, |
| | | executeHeightMode: null, |
| | | waylineId: null, |
| | | autoFlightSpeed: null, |
| | | /* |
| | | waylines.wpml和template.xml的Placemark通用 |
| | | 不同点: |
| | | waylineId = templateId |
| | | ellipsoidHeight、 height 替换成 executeHeight |
| | | */ |
| | | Placemark: null, |
| | | }, |
| | | } |
| | | Object.keys(waylinesObj).forEach(key => { |
| | | if (Object.prototype.toString.call(waylinesObj[key]) === '[object Object]') { |
| | | Object.keys(waylinesObj[key]).forEach(item => { |
| | | waylinesObj[key][item] = templateJson[key][item] |
| | | if (item === 'executeHeightMode') { |
| | | waylinesObj[key][item] = templateJson[key].waylineCoordinateSysParam.heightMode |
| | | } |
| | | if (item === 'waylineId') { |
| | | waylinesObj[key][item] = templateJson[key].templateId |
| | | } |
| | | if (item === 'Placemark') { |
| | | const placemarks = _.cloneDeep(templateJson[key][item]) |
| | | placemarks.forEach(placemark => { |
| | | placemark.executeHeight = { |
| | | '#text': placemark.ellipsoidHeight?.['#text'] || '', |
| | | } |
| | | delete placemark.ellipsoidHeight |
| | | delete placemark.height |
| | | }) |
| | | waylinesObj[key][item] = placemarks |
| | | } |
| | | }) |
| | | } else { |
| | | waylinesObj[key] = templateJson[key] |
| | | } |
| | | }) |
| | | return waylinesObj |
| | | } |
| | | |
| | | return { |
| | | create, |
| | | createNoramlWaylines, |
| | | createPlanarWayline, |
| | | createPointWayLines, |
| | | save, |
| | | } |
| | | } |
| | |
| | | import { useStore } from 'vuex' |
| | | import { getDeviceRegion, getDeviceRegionCount } from '@/api/home/aggregation' |
| | | import userStore from '@/store/modules/user' |
| | | import { areaCodeToArr, getContourByCode } from '@/utils/cesium/mapUtil' |
| | | // import { getEllipse } from '@/hooks/useSingleDroneMap/useSingleDroneMap' |
| | | import { areaCodeToArr, getContourByCode, getDockPolyLine, getLnglatAltitude } from '@/utils/cesium/mapUtil' |
| | | import getBaseConfig from '@/buildConfig/config' |
| | | import { getDroneStatusImage } from '@/utils/stateToImageMap/drone' |
| | | import { |
| | | GroundCirclePrimitiveManager |
| | | } from '@/utils/mapUtils' |
| | | import { GroundCirclePrimitiveManager } from '@/utils/mapUtils' |
| | | |
| | | import lowBattery from '@/assets/images/aiNowFly/low-battery.svg' |
| | | import mediumBattery from '@/assets/images/aiNowFly/medium-battery.svg' |
| | | import fullBattery from '@/assets/images/aiNowFly/full-charge.svg' |
| | | |
| | | const { VITE_APP_BASE, VITE_APP_ENV, VITE_APP_REGION_URL } = import.meta.env |
| | | // const { singleDockSystem } = getBaseConfig() |
| | | const { singleDockSystem } = getBaseConfig() |
| | | |
| | | |
| | | |
| | | /** |
| | | * 使用通用的边界 |
| | |
| | | * @param options |
| | | */ |
| | | export const useBoundary = (viewer, options = {}) => { |
| | | const { multiple = 1.4, dockOptions = {}, boundaryChange, boundaryColor, dockRangeType, useDockHeight = false } = options |
| | | const { scrollShowDock = false, showDock = false } = dockOptions |
| | | const { |
| | | multiple = 1.4, |
| | | dockOptions = {}, |
| | | boundaryChange, |
| | | boundaryColor, |
| | | dockRangeType, |
| | | useDockHeight = false, |
| | | } = options |
| | | const { scrollShowDock = false, showDock = false, showDockNameAndBattery = false } = dockOptions |
| | | |
| | | const manager = new GroundCirclePrimitiveManager() |
| | | manager.init(viewer) |
| | |
| | | } |
| | | |
| | | // 获取设备列表 |
| | | async function getDeviceRegionFun () { |
| | | async function getDeviceRegionFun() { |
| | | const res = await getDeviceRegion({ areaCode: selectedAreaCode }) |
| | | return res?.data?.data || [] |
| | | } |
| | |
| | | return await res.json() |
| | | } |
| | | |
| | | function getBase64Image(imgUrl) { |
| | | const img = new Image() |
| | | const canvas = document.createElement('canvas') |
| | | const ctx = canvas.getContext('2d') |
| | | |
| | | return new Promise(resolve => { |
| | | img.onload = function () { |
| | | canvas.width = img.width |
| | | canvas.height = img.height |
| | | ctx.drawImage(img, 0, 0) |
| | | const base64 = canvas.toDataURL('image/png') |
| | | resolve(base64) |
| | | } |
| | | img.src = imgUrl |
| | | }) |
| | | } |
| | | |
| | | function batteryLevelSvg(batteryNum) { |
| | | if (batteryNum <= 30) { |
| | | return lowBattery |
| | | } else if (batteryNum > 30 && batteryNum <= 60) { |
| | | return mediumBattery |
| | | } else { |
| | | return fullBattery |
| | | } |
| | | } |
| | | |
| | | async function nestSVG(nickname, batteryImgUrl, batteryNum) { |
| | | let txt = nickname.length < 4 ? 130 : nickname.length * 38 |
| | | let tranx = nickname.length < 4 ? 86 : nickname.length * 26 + 3 |
| | | const batteryBase64Image = await getBase64Image(batteryImgUrl) |
| | | const svg = ` |
| | | <svg width="${txt}" height="100" xmlns="http://www.w3.org/2000/svg"> |
| | | <g transform="translate(${tranx}, 10)"> |
| | | <!-- 电池图标 --> |
| | | <image href="${batteryBase64Image}" x="0" y="2" width="12" height="13"/> |
| | | <!-- 电量值 --> |
| | | <text x="12" y="10" font-size="14" fill="${ |
| | | batteryNum <= 30 ? '#FF604B' : batteryNum >= 60 ? '#40FF5C' : '#00FFF2' |
| | | }" text-anchor="start" dominant-baseline="middle" font-weight="bolder" font-family="Source Han Sans CN" text-rendering="geometricPrecision"> |
| | | ${batteryNum}% |
| | | </text> |
| | | </g> |
| | | </svg> |
| | | ` |
| | | return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}` |
| | | } |
| | | |
| | | // 无人机散点渲染 |
| | | const droneSplashed = () => { |
| | | let showDockList = showDockSnList === null |
| | | ? _.cloneDeep(initDockList) |
| | | : initDockList.filter(item => showDockSnList.includes(item.device_sn)) |
| | | manager.removeAll() |
| | | let showDockList = |
| | | showDockSnList === null |
| | | ? _.cloneDeep(initDockList) |
| | | : initDockList.filter(item => showDockSnList.includes(item.device_sn)) |
| | | |
| | | showDockList.forEach((item, index) => { |
| | | showDockList.forEach(async (item, index) => { |
| | | if (item.status === 'OFFLINE') return |
| | | let polyline = {} |
| | | |
| | | if (dockRangeType === 2) { |
| | | manager.addCircleOutline({ |
| | | data: { |
| | | lng: item.longitude, |
| | | lat: item.latitude |
| | | lat: item.latitude, |
| | | }, |
| | | frameColor: Cesium.Color.fromCssColorString('#7FFFD4') |
| | | frameColor: Cesium.Color.fromCssColorString('#7FFFD4'), |
| | | }) |
| | | } else { |
| | | manager.addCircle({ |
| | | data: { |
| | | lng: item.longitude, |
| | | lat: item.latitude |
| | | lat: item.latitude, |
| | | }, |
| | | materialColor: Cesium.Color.CORNFLOWERBLUE.withAlpha(0.3) |
| | | materialColor: item.color ? item.color : Cesium.Color.CORNFLOWERBLUE.withAlpha(0.3), |
| | | }) |
| | | } |
| | | |
| | | if (useDockHeight) { |
| | | polyline = { |
| | | positions: new Cesium.CallbackProperty(() => { |
| | | const pointPosition = Cesium.Cartesian3.fromDegrees(+item.longitude, +item.latitude, +item.height) |
| | | if (!pointPosition) return [] |
| | | // 获取点的位置 |
| | | const cartographic = Cesium.Cartographic.fromCartesian(pointPosition) |
| | | // 获取地形高度 |
| | | const terrainHeight = viewer.scene.globe.getHeight(cartographic) || 0 |
| | | // 创建地面点位置 |
| | | const groundPosition = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, terrainHeight) |
| | | |
| | | return [pointPosition, groundPosition] |
| | | }, false), |
| | | width: 0.5, |
| | | material: Cesium.Color.WHITE, |
| | | } |
| | | polyline = getDockPolyLine(item, viewer) |
| | | } |
| | | |
| | | const batteryImgUrl = batteryLevelSvg(item.capacity_percent) |
| | | const combinedImage = await nestSVG(item.nickname, batteryImgUrl, item.capacity_percent) |
| | | let pointObj = {} |
| | | try { |
| | | pointObj = await getLnglatAltitude(item.longitude, item.latitude, viewer) |
| | | } catch (e) {} |
| | | const position = Cesium.Cartesian3.fromDegrees( |
| | | +item.longitude, |
| | | +item.latitude, |
| | | useDockHeight ? +item.height : pointObj?.height || 0 |
| | | ) |
| | | if (showDockNameAndBattery) { |
| | | // 带电量 |
| | | dockSource.entities.add({ |
| | | position, |
| | | billboard: { |
| | | // new Cesium.ConstantProperty(pointData.status === 'OFFLINE' ? endingImg : unSelectedOnline), |
| | | image: combinedImage, |
| | | }, |
| | | }) |
| | | } |
| | | dockSource.entities.add({ |
| | | position: Cesium.Cartesian3.fromDegrees(+item.longitude, +item.latitude, !useDockHeight ? 0 : +item.height), |
| | | label: { |
| | | text: item.nickname, |
| | | font: 'bold 16px Source Han Sans CN', |
| | | fillColor: Cesium.Color.WHITE, |
| | | outlineColor: Cesium.Color.BLACK, |
| | | outlineWidth: 2, |
| | | style: Cesium.LabelStyle.FILL_AND_OUTLINE, |
| | | verticalOrigin: Cesium.VerticalOrigin.BOTTOM, |
| | | pixelOffset: new Cesium.Cartesian2(0, -24), |
| | | }, |
| | | position, |
| | | polyline, |
| | | billboard: { |
| | | image: getDroneStatusImage(item.status), |
| | | width: 60, |
| | | height: 60, |
| | | eyeOffset: new Cesium.Cartesian3(0, 0, -5) |
| | | disableDepthTestDistance: Number.POSITIVE_INFINITY, |
| | | eyeOffset: new Cesium.Cartesian3(0, 0, -5), |
| | | }, |
| | | }) |
| | | }) |
| | |
| | | positions: positions, |
| | | width: 5, // 直接设置宽度 |
| | | clampToGround: true, |
| | | material: Cesium.Color.fromCssColorString(boundaryColor) |
| | | material: Cesium.Color.fromCssColorString(boundaryColor), |
| | | }, |
| | | polygon, |
| | | }) |
| | |
| | | positions: positions, |
| | | width: 1, // 直接设置宽度 |
| | | clampToGround: true, |
| | | material: Cesium.Color.fromCssColorString(boundaryColor) |
| | | material: Cesium.Color.fromCssColorString(boundaryColor), |
| | | }, |
| | | }) |
| | | }) |
| | |
| | | // 打开边界 |
| | | const openContour = async () => { |
| | | const areaCode = selectedAreaCode || userAreaCode |
| | | |
| | | viewer.scene.postRender.removeEventListener(determineScaling) |
| | | if (!areaCode) return |
| | | const hierarchy = areaCodeToArr(areaCode.slice(0, 6)) |
| | |
| | | const outlineGJson = await getContourByCode(areaCode.slice(0, 6)) |
| | | scalingJudgment.forEach(item => item.show && (item.outline = outlineGJson)) |
| | | renderOutline(scalingJudgment[(hierarchy.length - 3) * -1]) |
| | | } catch (e) { } |
| | | viewer.scene.postRender.addEventListener(determineScaling) |
| | | } catch (e) {} |
| | | try { |
| | | viewer?.scene?.postRender?.addEventListener(determineScaling) |
| | | }catch (e) { |
| | | |
| | | } |
| | | } |
| | | |
| | | // 关闭边界 |
| | | function closeContour () { |
| | | function closeContour() { |
| | | removeAllEntities() |
| | | viewer.scene.postRender.removeEventListener(determineScaling) |
| | | } |
| | | |
| | | // 飞向边界 |
| | | async function flyToBoundary () { |
| | | async function flyToBoundary() { |
| | | // 单机巢系统 |
| | | if (singleDockSystem) { |
| | | if (!initDockList.length) { |
| | | initDockList = await getDeviceRegionFun() |
| | | } |
| | | viewer?.camera.flyTo({ |
| | | destination: Cesium.Cartesian3.fromDegrees(initDockList[0].longitude, initDockList[0].latitude, 24000), |
| | | destination: Cesium.Cartesian3.fromDegrees(initDockList[0].longitude, initDockList[0].latitude, 22000), |
| | | duration: 0, |
| | | }) |
| | | return |
| | |
| | | const dataSource = await Cesium.GeoJsonDataSource.load(gJson) |
| | | if (!dataSource) return |
| | | viewer.dataSources.add(dataSource) |
| | | |
| | | // 获取多边形边界所有点 |
| | | let positionList = [] |
| | | dataSource.entities.values.forEach(function (entity) { |
| | | if (entity.polygon) { |
| | | // 获取多边形的边界球 |
| | |
| | | let cartographic = Cesium.Cartographic.fromCartesian(item) |
| | | let lng = Cesium.Math.toDegrees(cartographic.longitude) // 经度 |
| | | let lat = Cesium.Math.toDegrees(cartographic.latitude) // 纬度 |
| | | |
| | | return [_.round(lng, 6), _.round(lat, 6)] |
| | | }) |
| | | |
| | | const newBox = boxTransformScale(curPolygonPosition, multiple) |
| | | |
| | | viewer.camera.flyTo({ |
| | | destination: Cesium.Rectangle.fromDegrees(...newBox), |
| | | offset: new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-90), 0), |
| | | duration: 0.5, |
| | | }) |
| | | positionList = positionList.concat(curPolygonPosition) |
| | | } |
| | | }) |
| | | const newBox = boxTransformScale(positionList, multiple) |
| | | viewer.camera.flyTo({ |
| | | destination: Cesium.Rectangle.fromDegrees(...newBox), |
| | | offset: new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-90), 0), |
| | | duration: 0.5, |
| | | }) |
| | | |
| | | dataSource.entities.values.forEach(entity => { |
| | |
| | | * 设置要显示的机巢列表 |
| | | * @param dockSns - 要显示的机巢设备序列号数组。如果为 undefined,则显示所有已初始化的机巢;如果为空数组,则不显示任何机巢。 |
| | | */ |
| | | function setShowDock (dockSns) { |
| | | function setShowDock(dockSns) { |
| | | if (dockSns === undefined) { |
| | | showDockSnList = null |
| | | } else if (dockSns.length === 0) { |
| | |
| | | } else { |
| | | showDockSnList = dockSns |
| | | } |
| | | |
| | | removeDockCover() |
| | | |
| | | if (active === '县') droneSplashed() |
| | | if (!showDock) return |
| | | if (scrollShowDock ? active === '县' : true) { |
| | | droneSplashed() |
| | | } |
| | | } |
| | | |
| | | function setDockCoverColor(arr) { |
| | | initDockList.forEach(item => { |
| | | arr.forEach(dock => { |
| | | item.color = dock.device_sn === item.device_sn ? dock.color : null |
| | | }) |
| | | }) |
| | | if (scrollShowDock ? active === '县' : true) { |
| | | droneSplashed() |
| | | } |
| | | } |
| | | onBeforeUnmount(() => { |
| | | manager.destroy() |
| | | }) |
| | | |
| | | |
| | | return { |
| | | openContour, |
| | | closeContour, |
| | | flyToBoundary, |
| | | setShowDock |
| | | setShowDock, |
| | | setDockCoverColor, |
| | | } |
| | | } |
| New file |
| | |
| | | <template> |
| | | <div class="center-container"> |
| | | <div class="left-box"> |
| | | <div class="logo"> |
| | | <img :src="warningLogo" alt=""> |
| | | </div> |
| | | |
| | | <div class="list"> |
| | | <div class="item" v-for="(item, ind) in warningList" :key="ind"> |
| | | <div class="val" @click="warningTypeClick(item.type)"> |
| | | {{ item.value }} |
| | | </div> |
| | | |
| | | <div class="name"> |
| | | {{ item.title }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="right-box"> |
| | | <div class="logo"> |
| | | <img :src="equipmentLogo" alt=""> |
| | | </div> |
| | | |
| | | <div class="list"> |
| | | <div class="item" v-for="(item, ind) in equipmentList" :key="ind"> |
| | | <div class="val" :class="{highlight: item.isHighlight}" @click="equipmentClick(item.isHighlight)"> |
| | | {{ item.value }} |
| | | </div> |
| | | |
| | | <div class="name"> |
| | | {{ item.title }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { onMounted, ref } from 'vue' |
| | | import warningLogo from '@/assets/images/dataCockpit/warning.png' |
| | | import equipmentLogo from '@/assets/images/dataCockpit/equipment.png' |
| | | import { alarmStatisticsApi } from '@/api/dataCockpit' |
| | | |
| | | const warningList = ref([ |
| | | { |
| | | title: '实时告警', |
| | | value: 0, |
| | | type: 1 |
| | | }, |
| | | { |
| | | title: '设备告警', |
| | | value: 0, |
| | | type: 2 |
| | | }, |
| | | { |
| | | title: '历史告警', |
| | | value: 0, |
| | | type: 3 |
| | | }, |
| | | ]) |
| | | |
| | | const equipmentList = ref([ |
| | | { |
| | | title: '设备统计', |
| | | value: 0, |
| | | isHighlight: true, |
| | | }, |
| | | { |
| | | title: '无线电', |
| | | value: 0, |
| | | }, |
| | | { |
| | | title: '光电', |
| | | value: 0, |
| | | }, |
| | | { |
| | | title: '雷达', |
| | | value: 0, |
| | | } |
| | | ]) |
| | | |
| | | const emit = defineEmits(['select-warning', 'select-equipment']) |
| | | |
| | | const warningTypeClick = (type) => { |
| | | emit('select-warning', type) |
| | | } |
| | | |
| | | const equipmentClick = (isHighlight) => { |
| | | if (isHighlight) { |
| | | emit('select-equipment') |
| | | } |
| | | } |
| | | |
| | | const updateStats = async () => { |
| | | const res = await alarmStatisticsApi() |
| | | const data = res?.data?.data || {} |
| | | warningList.value = [ |
| | | { title: '实时告警', value: data.realTimeAlarmCount ?? 0, type: 1 }, |
| | | { title: '设备告警', value: data.deviceAlarmCount ?? 0, type: 2 }, |
| | | { title: '历史告警', value: data.historyAlarmCount ?? 0, type: 3 } |
| | | ] |
| | | equipmentList.value = [ |
| | | { title: '设备统计', value: data.deviceCount ?? 0, isHighlight: true }, |
| | | { title: '无线电', value: data.radioCount ?? 0 }, |
| | | { title: '光电', value: data.opticalCount ?? 0 }, |
| | | { title: '雷达', value: data.radarCount ?? 0 } |
| | | ] |
| | | } |
| | | |
| | | onMounted(() => { |
| | | updateStats() |
| | | }) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .center-container { |
| | | display: flex; |
| | | position: absolute; |
| | | top: 75px; |
| | | left: 465px; |
| | | z-index: 9; |
| | | pointer-events: none; |
| | | |
| | | .left-box { |
| | | width: 356px; |
| | | |
| | | .list { |
| | | .item { |
| | | .val { |
| | | color: #FF4444; |
| | | cursor: pointer; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .right-box { |
| | | margin-left: 20px; |
| | | width: 455px; |
| | | |
| | | .list { |
| | | .item { |
| | | .val.highlight { |
| | | color: #6382FF; |
| | | cursor: pointer; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .left-box, |
| | | .right-box { |
| | | position: relative; |
| | | padding: 17px 20px; |
| | | display: flex; |
| | | align-items: center; |
| | | height: 100px; |
| | | background: rgba(0,0,0,0.3); |
| | | box-sizing: border-box; |
| | | border-radius: 16px 16px 16px 16px; |
| | | pointer-events: auto; |
| | | |
| | | &::after { |
| | | content: ''; |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | z-index: -1; |
| | | background: rgba(17, 23, 34, 0.56); |
| | | backdrop-filter: blur(2px); |
| | | border-radius: 16px 16px 16px 16px; |
| | | } |
| | | |
| | | |
| | | .logo { |
| | | img { |
| | | width: 40px; |
| | | height: 40px; |
| | | } |
| | | } |
| | | |
| | | .list { |
| | | width: 0; |
| | | flex: 1; |
| | | height: 100%; |
| | | display: flex; |
| | | |
| | | .item { |
| | | width: 0; |
| | | flex: 1; |
| | | position: relative; |
| | | padding-left: 20px; |
| | | color: #fff; |
| | | |
| | | .name { |
| | | font-family: Open Sans, Open Sans; |
| | | font-weight: 400; |
| | | font-size: 14px; |
| | | color: #FFFFFF; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | |
| | | .val { |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: bold; |
| | | font-size: 30px; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | |
| | | &::after { |
| | | content: ''; |
| | | position: absolute; |
| | | top: 0; |
| | | right: 0; |
| | | width: 1px; |
| | | height: 100%; |
| | | width: 1px; |
| | | background: #333355; |
| | | } |
| | | |
| | | &:last-child::after { |
| | | display: none; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <el-dialog class="ztzf-data-cockpit-dialog" append-to-body v-model="visibleModel" :title="dialogTitle" |
| | | :close-on-click-modal="false" :destroy-on-close="true"> |
| | | <el-form ref="queryParamsRef" :model="searchParams" class="ztzf-dialog-history-search"> |
| | | <el-form-item label="关键字" prop="keyword"> |
| | | <el-input class="ztzf-data-cockpit-search-input" v-model="searchParams.keyword" placeholder="名称/序列号" |
| | | clearable @clear="handleSearch" /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item prop="dateRange"> |
| | | <el-date-picker class="ztzf-data-cockpit-date-picker" |
| | | popper-class="ztzf-data-cockpit-tree-select-popper" v-model="searchParams.dateRange" |
| | | type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" |
| | | value-format="YYYY/MM/DD" @change="handleSearch" /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="区域" prop="area"> |
| | | <el-tree-select class="ztzf-data-cockpit-select" popper-class="ztzf-data-cockpit-tree-select-popper" |
| | | v-model="searchParams.area" :data="areaTree" :props="areaTreeProps" node-key="value" check-strictly |
| | | clearable placeholder="请选择" @change="handleSearch" /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="无人机类型" prop="droneType"> |
| | | <el-select class="ztzf-data-cockpit-select" popper-class="ztzf-data-cockpit-select-popper" |
| | | v-model="searchParams.droneType" placeholder="请选择" clearable @change="handleSearch"> |
| | | <el-option v-for="item in droneTypeOptions" :key="item.value" :label="item.label" |
| | | :value="item.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item class="history-search-actions"> |
| | | <el-button :icon="RefreshRight" @click="resetForm"></el-button> |
| | | <el-button :icon="Search" @click="handleSearch"></el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <div class="ztzf-table-container" v-loading="loading" element-loading-background="rgba(5, 5, 15, 0.6)"> |
| | | <div class="ztzf-table-content"> |
| | | <el-table class="ztzf-data-cockpit-table" :data="list"> |
| | | <el-table-column type="index" width="60" label="序号" /> |
| | | <el-table-column prop="droneName" label="无人机名称" min-width="120" /> |
| | | <el-table-column prop="droneSerialNo" label="无人机序列号" min-width="140" /> |
| | | <el-table-column label="无人机类型" min-width="110"> |
| | | <template #default="{ row }"> |
| | | {{ getDroneTypeText(row.droneType) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="areaName" label="区域" min-width="90" /> |
| | | <el-table-column prop="deviceName" label="数据来源" min-width="110" /> |
| | | <el-table-column label="信号频段" min-width="100"> |
| | | <template #default="{ row }"> |
| | | {{ formatFrequency(row.signalFreqMhz) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="alarmTime" label="发现时间" min-width="150" /> |
| | | <el-table-column prop="stayDuration" label="停留时间" min-width="110" /> |
| | | <el-table-column label="反制方式" min-width="110"> |
| | | <template #default="{ row }"> |
| | | {{ getCounterWayText(row.counterWay) }} |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | |
| | | <div class="ztzf-table-pagination"> |
| | | <el-pagination popper-class="ztzf-data-cockpit-select-popper" |
| | | v-model:current-page="searchParams.current" v-model:page-size="searchParams.size" |
| | | layout="total, prev, pager, next, sizes" :total="total" @change="getList" /> |
| | | </div> |
| | | </div> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { Search, RefreshRight } from '@element-plus/icons-vue' |
| | | import { computed, ref, watch } from 'vue' |
| | | import { alarmLogApi } from '@/api/dataCockpit' |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { |
| | | type: Boolean, |
| | | default: false |
| | | }, |
| | | device: { |
| | | type: Object, |
| | | default: () => null |
| | | } |
| | | }) |
| | | |
| | | const emit = defineEmits(['update:modelValue']) |
| | | |
| | | const visibleModel = computed({ |
| | | get: () => props.modelValue, |
| | | set: (val) => emit('update:modelValue', val) |
| | | }) |
| | | |
| | | const dialogTitle = computed(() => { |
| | | return `反制设备名称 ${props.device?.name || ''}`.trim() |
| | | }) |
| | | |
| | | const initSearchParams = () => ({ |
| | | keyword: '', |
| | | dateRange: [], |
| | | area: '', |
| | | droneType: '', |
| | | current: 1, |
| | | size: 10 |
| | | }) |
| | | const searchParams = ref(initSearchParams()) |
| | | const queryParamsRef = ref(null) |
| | | const loading = ref(true) |
| | | const list = ref([]) |
| | | const total = ref(0) |
| | | const areaTree = ref([ |
| | | { |
| | | label: '古村区', |
| | | value: '古村区' |
| | | }, |
| | | { |
| | | label: '新区', |
| | | value: '新区' |
| | | }, |
| | | { |
| | | label: '产业园', |
| | | value: '产业园' |
| | | } |
| | | ]) |
| | | const areaTreeProps = { |
| | | label: 'label', |
| | | children: 'children' |
| | | } |
| | | |
| | | const droneTypeOptions = [ |
| | | { label: '微型机', value: '1' }, |
| | | { label: '植保机', value: '2' } |
| | | ] |
| | | |
| | | const getList = async () => { |
| | | if (!props.device?.id) { |
| | | list.value = [] |
| | | total.value = 0 |
| | | return |
| | | } |
| | | const [startTime, endTime] = searchParams.value.dateRange || [] |
| | | const keyword = (searchParams.value.keyword || '').trim() |
| | | const params = { |
| | | alarmType: '2', |
| | | deviceId: props.device.id, |
| | | current: searchParams.value.current, |
| | | size: searchParams.value.size, |
| | | startTime, |
| | | endTime, |
| | | areaName: searchParams.value.area || undefined, |
| | | droneType: searchParams.value.droneType || undefined, |
| | | droneName: keyword || undefined, |
| | | } |
| | | loading.value = true |
| | | try { |
| | | const res = await alarmLogApi(params) |
| | | list.value = res?.data?.data?.records ?? [] |
| | | total.value = res?.data?.data?.total ?? 0 |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | function handleSearch () { |
| | | searchParams.value.current = 1 |
| | | getList() |
| | | } |
| | | |
| | | function resetForm () { |
| | | searchParams.value = initSearchParams() |
| | | |
| | | getList() |
| | | } |
| | | |
| | | const formatFrequency = (value) => { |
| | | if (value == null || value === '') return '-' |
| | | return `${value}MHZ` |
| | | } |
| | | |
| | | const getCounterWayText = (value) => { |
| | | if (value === 1 || value === '1') return '信号干扰' |
| | | if (value === 2 || value === '2') return '诱导驱离' |
| | | if (value === 3 || value === '3') return '无' |
| | | return value || '-' |
| | | } |
| | | |
| | | const getDroneTypeText = (value) => { |
| | | if (value === 1 || value === '1') return '微型机' |
| | | if (value === 2 || value === '2') return '植保机' |
| | | return value || '-' |
| | | } |
| | | |
| | | watch( |
| | | () => visibleModel.value, |
| | | (visible) => { |
| | | if (visible) { |
| | | searchParams.value.current = 1 |
| | | getList() |
| | | } |
| | | } |
| | | ) |
| | | |
| | | </script> |
| | | |
| | | <style lang="scss" scoped></style> |
| New file |
| | |
| | | <!-- |
| | | * @Author : yuan |
| | | * @Date : 2026-01-09 15:06:02 |
| | | * @LastEditors : yuan |
| | | * @LastEditTime : 2026-01-09 15:10:44 |
| | | * @FilePath : \applications\drone-command\src\views\dataCockpit\components\EmptyState.vue |
| | | * @Description : |
| | | * Copyright 2026 OBKoro1, All Rights Reserved. |
| | | * 2026-01-09 15:06:02 |
| | | --> |
| | | <template> |
| | | <div class="empty-state"> |
| | | <div class="empty-content"> |
| | | <img :src="noDataBg" alt="暂无数据"> |
| | | <span class="empty-text">暂无数据</span> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import noDataBg from '@/assets/images/no-data-bg.png' |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .empty-state { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | flex: 1; |
| | | width: 100%; |
| | | height: 100%; |
| | | min-height: 220px; |
| | | |
| | | .empty-content { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | flex-direction: column; |
| | | text-align: center; |
| | | } |
| | | |
| | | img { |
| | | max-width: 78%; |
| | | max-height: 78%; |
| | | object-fit: contain; |
| | | } |
| | | |
| | | .empty-text { |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: 400; |
| | | font-size: 14px; |
| | | color: #9E9EBA; |
| | | line-height: 18px; |
| | | text-align: center; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | } |
| | | </style> |
| New file |
| | |
| | | <!-- |
| | | * @Author : yuan |
| | | * @Date : 2026-01-08 09:29:07 |
| | | * @LastEditors : yuan |
| | | * @LastEditTime : 2026-01-08 14:49:25 |
| | | * @FilePath : \applications\drone-command\src\views\dataCockpit\components\EquipmentWarning.vue |
| | | * @Description : |
| | | * Copyright 2026 OBKoro1, All Rights Reserved. |
| | | * 2026-01-08 09:29:07 |
| | | --> |
| | | <template> |
| | | <div |
| | | class="equipment-warning" |
| | | v-loading="loading" |
| | | element-loading-background="rgba(5, 5, 15, 0.6)" |
| | | element-loading-text="加载中..." |
| | | > |
| | | <EmptyState v-if="!equipmentWarningData.length" /> |
| | | <EquipmentTemplate |
| | | v-else |
| | | v-for="(item, ind) in equipmentWarningData" |
| | | :key="ind" |
| | | :data="item" |
| | | @click="onCardClick" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { onMounted, ref } from 'vue' |
| | | import EquipmentTemplate from './templateComponents/EquipmentTemplate.vue' |
| | | import { deviceSearchApi } from '@/api/dataCockpit' |
| | | import EmptyState from './EmptyState.vue' |
| | | |
| | | const equipmentWarningData = ref([]) |
| | | const loading = ref(true) |
| | | const minLoadingMs = 400 |
| | | |
| | | const statusMap = { |
| | | 0: { text: '在线', statusType: 'online' }, |
| | | 1: { text: '离线', statusType: 'offline' }, |
| | | 2: { text: '故障', statusType: 'fault' }, |
| | | 3: { text: '报废', statusType: 'offline' } |
| | | } |
| | | const deviceTypeMap = { |
| | | 1: '便捷侦测箱', |
| | | 2: '反制枪', |
| | | 3: '察打一体' |
| | | } |
| | | |
| | | const formatDeviceItem = (item) => { |
| | | const statusInfo = statusMap[item?.status] || { text: '-', statusType: '' } |
| | | return { |
| | | id: item.id, |
| | | name: item.deviceName || item.deviceModel || '-', |
| | | status: statusInfo.text, |
| | | statusType: statusInfo.statusType, |
| | | type: deviceTypeMap[item.deviceType] || item.deviceType || '-', |
| | | manufacturer: item.manufacturer || '-', |
| | | azimuth: item.azimuth ?? 0, |
| | | pitch: item.elevation ?? 0, |
| | | range: item.effectiveRangeKm ?? 0, |
| | | model: item.deviceModel || '-', |
| | | longitude: item.longitude ?? '-', |
| | | latitude: item.latitude ?? '-' |
| | | } |
| | | } |
| | | |
| | | const fetchDeviceWarning = async () => { |
| | | const startAt = Date.now() |
| | | loading.value = true |
| | | try { |
| | | const params = { current: 1, size: 20, deviceStatusList: '1,2,3' } |
| | | const res = await deviceSearchApi(params) |
| | | const records = res?.data?.data?.records ?? [] |
| | | equipmentWarningData.value = records.map(formatDeviceItem) |
| | | } finally { |
| | | const elapsed = Date.now() - startAt |
| | | if (elapsed < minLoadingMs) { |
| | | await new Promise((resolve) => setTimeout(resolve, minLoadingMs - elapsed)) |
| | | } |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | fetchDeviceWarning() |
| | | }) |
| | | |
| | | const onCardClick = (item) => { |
| | | console.log('点击设备卡片:', item.id) |
| | | } |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .equipment-warning { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <div |
| | | class="history-warning" |
| | | v-loading="loading" |
| | | element-loading-background="rgba(5, 5, 15, 0.6)" |
| | | element-loading-text="加载中..." |
| | | > |
| | | <!-- 搜索区(设计稿红框) --> |
| | | <div class="search-box"> |
| | | <!-- 关键字 --> |
| | | <el-input v-model="query.keyword" class="ztzf-data-cockpit-search-input" placeholder="请输入数据名称" clearable |
| | | @keyup.enter="onSearch"> |
| | | </el-input> |
| | | |
| | | <!-- 日期范围 --> |
| | | <el-date-picker v-model="query.dateRange" |
| | | class="ztzf-data-cockpit-date-picker" |
| | | popper-class="ztzf-data-cockpit-date-picker-popper" |
| | | type="daterange" |
| | | range-separator="~" |
| | | start-placeholder="创建开始日期" end-placeholder="结束日期" format="YYYY年MM月DD日" value-format="YYYY-MM-DD" |
| | | :clearable="true" @change="onSearch"> |
| | | </el-date-picker> |
| | | </div> |
| | | |
| | | <!-- 列表 --> |
| | | <div class="list-content"> |
| | | <EmptyState v-if="!filteredList.length" /> |
| | | <HistoryTemplate |
| | | v-else |
| | | v-for="(item, ind) in filteredList" |
| | | :key="item.id || ind" |
| | | :data="item" |
| | | @click="onCardClick" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, onMounted } from 'vue' |
| | | import HistoryTemplate from './templateComponents/HistoryTemplate.vue' |
| | | import { alarmLogApi } from '@/api/dataCockpit' |
| | | import EmptyState from './EmptyState.vue' |
| | | |
| | | const historyWarningData = ref([]) |
| | | const loading = ref(true) |
| | | const minLoadingMs = 400 |
| | | |
| | | // 查询条件:keyword + dateRange(YYYY-MM-DD数组) |
| | | const query = ref({ |
| | | keyword: '', |
| | | dateRange: [] // [start, end] |
| | | }) |
| | | |
| | | const onSearch = () => { |
| | | fetchHistoryWarning() |
| | | } |
| | | |
| | | const onCardClick = (item) => { |
| | | console.log('点击无人机卡片:', item.id) |
| | | } |
| | | |
| | | const fetchHistoryWarning = async () => { |
| | | const startAt = Date.now() |
| | | loading.value = true |
| | | try { |
| | | const [startTime, endTime] = query.value.dateRange || [] |
| | | const keyword = (query.value.keyword || '').trim() |
| | | const params = { |
| | | current: 1, |
| | | size: 20, |
| | | alarmType: '2', |
| | | startTime, |
| | | endTime, |
| | | droneName: keyword || undefined, |
| | | droneSerialNo: keyword || undefined |
| | | } |
| | | const res = await alarmLogApi(params) |
| | | const records = res?.data?.data?.records ?? [] |
| | | historyWarningData.value = records |
| | | } finally { |
| | | const elapsed = Date.now() - startAt |
| | | if (elapsed < minLoadingMs) { |
| | | await new Promise((resolve) => setTimeout(resolve, minLoadingMs - elapsed)) |
| | | } |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | const filteredList = computed(() => { |
| | | return historyWarningData.value |
| | | }) |
| | | |
| | | onMounted(() => { |
| | | fetchHistoryWarning() |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .history-warning { |
| | | display: flex; |
| | | flex-direction: column; |
| | | width: 100%; |
| | | height: 100%; |
| | | |
| | | .search-box { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | |
| | | ::v-deep(.ztzf-data-cockpit-date-picker) { |
| | | width: 100% !important; |
| | | } |
| | | } |
| | | |
| | | .list-content { |
| | | height: 0; |
| | | flex: 1; |
| | | } |
| | | } |
| | | |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <div class="left-container" :class="{ collapsed: collapsedModel }"> |
| | | <div class="wrapper"> |
| | | <TitleTemplate>异常情况告警</TitleTemplate> |
| | | |
| | | <div class="category-container"> |
| | | <div class="category-item" v-for="(item, ind) in categoryList" :key="ind" |
| | | @click="activeIdModel = item.id" :class="{ active: activeIdModel === item.id }"> |
| | | {{ item.name }} |
| | | <span class="badge" v-if="item.badge">{{ item.badge }}</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="content"> |
| | | <component :is="currentComponent" /> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="collapse-btn" @click="toggleCollapse"> |
| | | <span class="arrow" :class="collapsedModel ? 'right' : 'left'"></span> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import TitleTemplate from './templateComponents/TitleTemplate.vue' |
| | | import RealWarning from './RealWarning.vue' |
| | | import EquipmentWarning from './EquipmentWarning.vue' |
| | | import HistoryWarning from './HistoryWarning.vue' |
| | | import { alarmStatisticsApi } from '@/api/dataCockpit' |
| | | import { onMounted } from 'vue' |
| | | |
| | | const props = defineProps({ |
| | | activeId: { |
| | | type: Number, |
| | | default: 1 |
| | | }, |
| | | collapsed: { |
| | | type: Boolean, |
| | | default: false |
| | | } |
| | | }) |
| | | |
| | | const emit = defineEmits(['update:activeId', 'update:collapsed']) |
| | | |
| | | const activeIdModel = computed({ |
| | | get: () => props.activeId, |
| | | set: (val) => emit('update:activeId', val) |
| | | }) |
| | | |
| | | const collapsedModel = computed({ |
| | | get: () => props.collapsed, |
| | | set: (val) => emit('update:collapsed', val) |
| | | }) |
| | | |
| | | const categoryList = ref([ |
| | | { |
| | | id: 1, |
| | | name: '实时告警', |
| | | badge: 0, |
| | | component: RealWarning |
| | | }, |
| | | { |
| | | id: 2, |
| | | name: '设备告警', |
| | | badge: 0, |
| | | component: EquipmentWarning |
| | | }, |
| | | { |
| | | id: 3, |
| | | name: '历史告警', |
| | | badge: 0, |
| | | component: HistoryWarning |
| | | } |
| | | ]) |
| | | |
| | | const currentComponent = computed(() => { |
| | | return categoryList.value.find(i => i.id === activeIdModel.value)?.component |
| | | }) |
| | | |
| | | const toggleCollapse = () => { |
| | | collapsedModel.value = !collapsedModel.value |
| | | } |
| | | |
| | | const updateBadges = async () => { |
| | | const res = await alarmStatisticsApi() |
| | | const data = res?.data?.data || {} |
| | | const realTotal = data.realTimeAlarmCount ?? 0 |
| | | const historyTotal = data.historyAlarmCount ?? 0 |
| | | const deviceTotal = data.deviceAlarmCount ?? 0 |
| | | categoryList.value = categoryList.value.map((item) => { |
| | | if (item.id === 1) return { ...item, badge: realTotal } |
| | | if (item.id === 2) return { ...item, badge: deviceTotal } |
| | | if (item.id === 3) return { ...item, badge: historyTotal } |
| | | return item |
| | | }) |
| | | } |
| | | |
| | | onMounted(() => { |
| | | updateBadges() |
| | | }) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .left-container { |
| | | position: relative; |
| | | transition: transform 0.3s ease-in-out; |
| | | |
| | | &.collapsed { |
| | | transform: translateX(-100%); |
| | | } |
| | | } |
| | | |
| | | .collapse-btn { |
| | | position: absolute; |
| | | top: 50%; |
| | | left: 357px; |
| | | cursor: pointer; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | color: #fff; |
| | | z-index: 10; |
| | | transform: translateY(-50%); |
| | | |
| | | .arrow { |
| | | width: 24px; |
| | | height: 24px; |
| | | background: url('@/assets/images/dataCockpit/left-btn.png') center / 100% 100% no-repeat; |
| | | } |
| | | |
| | | &:hover { |
| | | background: rgba(40, 79, 227, 0.6); |
| | | } |
| | | } |
| | | |
| | | .category-container { |
| | | position: relative; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | |
| | | &::after { |
| | | content: ''; |
| | | position: absolute; |
| | | left: 0; |
| | | bottom: 0; |
| | | width: 100%; |
| | | height: 1px; |
| | | background: #333355; |
| | | } |
| | | |
| | | .category-item { |
| | | position: relative; |
| | | width: 0; |
| | | flex: 1; |
| | | margin-left: 26px; |
| | | height: 68px; |
| | | line-height: 68px; |
| | | |
| | | font-family: Source Code Pro, Source Code Pro; |
| | | font-weight: 400; |
| | | font-size: 14px; |
| | | color: #86909C; |
| | | text-align: center; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | cursor: pointer; |
| | | |
| | | &:first-child { |
| | | margin-left: 0; |
| | | } |
| | | |
| | | &.active { |
| | | font-weight: normal; |
| | | color: #FFFFFF; |
| | | } |
| | | |
| | | &::after { |
| | | content: ''; |
| | | position: absolute; |
| | | left: 0; |
| | | bottom: 0; |
| | | width: 100%; |
| | | height: 0; |
| | | } |
| | | |
| | | &.active::after { |
| | | height: 3px; |
| | | background: #284FE3; |
| | | box-shadow: 0px 3px 2px 0px rgba(15, 89, 255, 0.1), 0px 7px 5px 0px rgba(15, 89, 255, 0.15), 0px 13px 10px 0px rgba(15, 89, 255, 0.18), 0px 22px 18px 0px rgba(15, 89, 255, 0.21), 0px 42px 33px 0px rgba(15, 89, 255, 0.26), 0px 100px 80px 0px rgba(15, 89, 255, 0.36); |
| | | border-radius: 5px 5px 0px 0px; |
| | | z-index: 9; |
| | | } |
| | | |
| | | .badge { |
| | | position: absolute; |
| | | top: 16px; |
| | | right: 4px; |
| | | min-width: 18px; |
| | | line-height: 18px; |
| | | font-size: 12px; |
| | | color: #FFFFFF; |
| | | background: #FF3B30; |
| | | border-radius: 50%; |
| | | text-align: center; |
| | | box-shadow: 0px 2px 6px rgba(255, 59, 48, 0.6); |
| | | pointer-events: none; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .content { |
| | | height: 0; |
| | | flex: 1; |
| | | margin-top: 20px; |
| | | overflow-x: hidden; |
| | | overflow-y: auto; |
| | | } |
| | | </style> |
| New file |
| | |
| | | <!-- |
| | | * @Author : yuan |
| | | * @Date : 2026-01-08 09:29:07 |
| | | * @LastEditors : yuan |
| | | * @LastEditTime : 2026-01-08 14:49:09 |
| | | * @FilePath : \applications\drone-command\src\views\dataCockpit\components\RealWarning.vue |
| | | * @Description : |
| | | * Copyright 2026 OBKoro1, All Rights Reserved. |
| | | * 2026-01-08 09:29:07 |
| | | --> |
| | | <template> |
| | | <div |
| | | class="real-warning" |
| | | v-loading="loading" |
| | | element-loading-background="rgba(5, 5, 15, 0.6)" |
| | | element-loading-text="加载中..." |
| | | > |
| | | <EmptyState v-if="!realWarningData.length" /> |
| | | <RealTemplate |
| | | v-else |
| | | v-for="(item, ind) in realWarningData" |
| | | :key="ind" |
| | | :data="item" |
| | | @signal="() => handleAction('signal', item)" |
| | | @counter="() => handleAction('counter', item)" |
| | | @favorite="() => console.log('收藏', item.id)" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import RealTemplate from './templateComponents/RealTemplate.vue' |
| | | import { alarmLogApi, interferenceAndExpulsionApi } from '@/api/dataCockpit' |
| | | import { onMounted, ref } from 'vue' |
| | | import EmptyState from './EmptyState.vue' |
| | | import { ElMessage } from 'element-plus' |
| | | |
| | | const realWarningData = ref([]) |
| | | const loading = ref(true) |
| | | const minLoadingMs = 400 |
| | | |
| | | const actionTextMap = { |
| | | signal: '信号干扰', |
| | | counter: '诱导驱离' |
| | | } |
| | | |
| | | const buildPayload = (item) => ({ |
| | | alarmRecordId: item.id, |
| | | areaCode: item.areaCode, |
| | | counterEffect: '', |
| | | coverRadiusM: item.coverRadiusM, |
| | | deployLatitude: item.latitude, |
| | | deployLongitude: item.longitude, |
| | | deviceCode: item.deviceCode, |
| | | deviceId: item.deviceId, |
| | | deviceModel: item.deviceModel, |
| | | deviceName: item.deviceName, |
| | | deviceType: item.deviceType, |
| | | droneDeviceCode: item.droneSerialNo, |
| | | droneName: item.droneName, |
| | | droneType: item.droneType, |
| | | findTime: item.alarmTime, |
| | | id: item.id, |
| | | workMode: '' |
| | | }) |
| | | |
| | | const handleAction = async (type, item) => { |
| | | const actionText = actionTextMap[type] || '操作' |
| | | try { |
| | | const res = await interferenceAndExpulsionApi(buildPayload(item)) |
| | | if (res?.data?.success) { |
| | | ElMessage({ type: 'success', message: `${actionText}成功` }) |
| | | } else { |
| | | ElMessage({ type: 'error', message: res?.data?.msg || `${actionText}失败` }) |
| | | } |
| | | } catch (error) { |
| | | ElMessage({ type: 'error', message: `${actionText}失败` }) |
| | | } |
| | | } |
| | | |
| | | const fetchRealWarning = async () => { |
| | | const startAt = Date.now() |
| | | loading.value = true |
| | | try { |
| | | const params = { current: 1, size: 20, alarmType: '1' } |
| | | const res = await alarmLogApi(params) |
| | | const records = res?.data?.data?.records ?? [] |
| | | realWarningData.value = records |
| | | } finally { |
| | | const elapsed = Date.now() - startAt |
| | | if (elapsed < minLoadingMs) { |
| | | await new Promise((resolve) => setTimeout(resolve, minLoadingMs - elapsed)) |
| | | } |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | fetchRealWarning() |
| | | }) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .real-warning { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | |
| | | </style> |
| New file |
| | |
| | | <!-- |
| | | * @Author : yuan |
| | | * @Date : 2026-01-07 15:17:54 |
| | | * @LastEditors : yuan |
| | | * @LastEditTime : 2026-01-09 16:54:31 |
| | | * @FilePath : \applications\drone-command\src\views\dataCockpit\components\RightContainer.vue |
| | | * @Description : |
| | | * Copyright 2026 OBKoro1, All Rights Reserved. |
| | | * 2026-01-07 15:17:54 |
| | | --> |
| | | <template> |
| | | <div class="right-container" :class="{ collapsed: collapsedModel }"> |
| | | <div class="wrapper"> |
| | | <TitleTemplate>侦测反制设备</TitleTemplate> |
| | | |
| | | <div class="category-container"> |
| | | <div class="category-item" v-for="(item, ind) in categoryList" :key="ind"> |
| | | <div class="category-item-logo"> |
| | | <img :src="item.logo" alt=""> |
| | | </div> |
| | | |
| | | <div class="category-item-content"> |
| | | <span class="name">{{ item.name }}</span> |
| | | <span class="val">{{ item.val }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div |
| | | class="content" |
| | | v-loading="loading" |
| | | element-loading-background="rgba(5, 5, 15, 0.6)" |
| | | element-loading-text="加载中..." |
| | | > |
| | | <EmptyState v-if="!counterDeviceStatusList.length" /> |
| | | <RealEquipmentTemplate |
| | | v-else |
| | | v-for="item in counterDeviceStatusList" |
| | | :key="item.id" |
| | | :data="item" |
| | | @history="onHistory" |
| | | @click="onCardClick" |
| | | /> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="collapse-btn" @click="toggleCollapse"> |
| | | <span class="arrow" :class="collapsedModel ? 'left' : 'right'"></span> |
| | | </div> |
| | | |
| | | <DeviceHistoryDialog v-model="historyVisible" :device="historyDevice" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import TitleTemplate from './templateComponents/TitleTemplate.vue' |
| | | import RealEquipmentTemplate from './templateComponents/RealEquipmentTemplate.vue' |
| | | import DeviceHistoryDialog from './DeviceHistoryDialog.vue' |
| | | import EmptyState from './EmptyState.vue' |
| | | import zcsbLogo from '@/assets/images/dataCockpit/zcsb.png' |
| | | import fzsbLogo from '@/assets/images/dataCockpit/fzsb.png' |
| | | import { deviceSearchApi, deviceStatisticsApi } from '@/api/dataCockpit' |
| | | |
| | | const props = defineProps({ |
| | | collapsed: { |
| | | type: Boolean, |
| | | default: false |
| | | } |
| | | }) |
| | | |
| | | const emit = defineEmits(['update:collapsed', 'update:onlineDevices']) |
| | | |
| | | const collapsedModel = computed({ |
| | | get: () => props.collapsed, |
| | | set: (val) => emit('update:collapsed', val) |
| | | }) |
| | | |
| | | const categoryList = ref([ |
| | | { |
| | | id: 1, |
| | | name: '侦测设备', |
| | | val: 0, |
| | | logo: zcsbLogo |
| | | }, |
| | | { |
| | | id: 2, |
| | | name: '反制设备', |
| | | val: 0, |
| | | logo: fzsbLogo |
| | | } |
| | | ]) |
| | | |
| | | const counterDeviceStatusList = ref([]) |
| | | |
| | | const historyVisible = ref(false) |
| | | const historyDevice = ref(null) |
| | | const loading = ref(true) |
| | | const minLoadingMs = 400 |
| | | |
| | | const onHistory = (item) => { |
| | | historyDevice.value = item |
| | | historyVisible.value = true |
| | | console.log('历史数据:', item.id) |
| | | } |
| | | |
| | | const onCardClick = (item) => { |
| | | console.log('点击卡片:', item.id) |
| | | } |
| | | const toggleCollapse = () => { |
| | | collapsedModel.value = !collapsedModel.value |
| | | } |
| | | |
| | | const statusMap = { |
| | | 0: { text: '在线', actionType: 'detecting' }, |
| | | 1: { text: '离线', actionType: '' }, |
| | | 2: { text: '故障', actionType: 'jamming' }, |
| | | 3: { text: '报废', actionType: '' } |
| | | } |
| | | const deviceTypeMap = { |
| | | 1: '便捷侦测箱', |
| | | 2: '反制枪', |
| | | 3: '察打一体' |
| | | } |
| | | |
| | | const formatDeviceItem = (item) => { |
| | | const statusInfo = statusMap[item?.status] || { text: '-', actionType: '' } |
| | | return { |
| | | id: item.id, |
| | | name: item.deviceName || item.deviceModel || '-', |
| | | actionText: statusInfo.text, |
| | | actionType: statusInfo.actionType, |
| | | type: deviceTypeMap[item.deviceType] || item.deviceType || '-', |
| | | batteryPct: item.batteryPct ?? 0, |
| | | azimuth: item.azimuth ?? 0, |
| | | elevation: item.elevation ?? 0, |
| | | status: item.status ?? '-', |
| | | manufacturer: item.manufacturer || '-', |
| | | detectTargetCnt: item.detectTargetCnt ?? 0, |
| | | effectiveRangeKm: item.effectiveRangeKm ?? 0 |
| | | } |
| | | } |
| | | |
| | | const fetchDeviceList = async () => { |
| | | const startAt = Date.now() |
| | | loading.value = true |
| | | try { |
| | | const params = { current: 1, size: 20, deviceStatusList: '0' } |
| | | const res = await deviceSearchApi(params) |
| | | const records = res?.data?.data?.records ?? [] |
| | | counterDeviceStatusList.value = records.map(formatDeviceItem) |
| | | emit('update:onlineDevices', records) |
| | | } finally { |
| | | const elapsed = Date.now() - startAt |
| | | if (elapsed < minLoadingMs) { |
| | | await new Promise((resolve) => setTimeout(resolve, minLoadingMs - elapsed)) |
| | | } |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | fetchDeviceList() |
| | | fetchDeviceStatistics() |
| | | }) |
| | | |
| | | const normalizeType = (value) => { |
| | | if (value === 1 || value === '1') return '侦测设备' |
| | | if (value === 2 || value === '2') return '反制设备' |
| | | if (value === 'detect') return '侦测设备' |
| | | if (value === 'counter') return '反制设备' |
| | | return value |
| | | } |
| | | |
| | | const fetchDeviceStatistics = async () => { |
| | | const res = await deviceStatisticsApi() |
| | | const list = res?.data?.data ?? [] |
| | | const map = new Map(list.map((item) => [normalizeType(item.type), item.count])) |
| | | categoryList.value = categoryList.value.map((item) => ({ |
| | | ...item, |
| | | val: map.has(item.name) ? map.get(item.name) : item.val |
| | | })) |
| | | } |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .category-container { |
| | | margin-top: 20px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 28px; |
| | | box-sizing: border-box; |
| | | |
| | | .category-item { |
| | | position: relative; |
| | | width: 0; |
| | | flex: 1; |
| | | display: flex; |
| | | height: 64px; |
| | | |
| | | &-logo { |
| | | display: flex; |
| | | align-items: center; |
| | | |
| | | img { |
| | | width: 40px; |
| | | height: 40px; |
| | | } |
| | | } |
| | | |
| | | &-content { |
| | | width: 0; |
| | | flex: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | border-bottom: 1px solid #6C6C89; |
| | | |
| | | span { |
| | | margin-left: 8px; |
| | | } |
| | | |
| | | .name { |
| | | font-family: Open Sans, Open Sans; |
| | | font-weight: 400; |
| | | font-size: 16px; |
| | | color: #C3C3DD; |
| | | line-height: 20px; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | |
| | | .val { |
| | | margin-top: 10px; |
| | | font-family: DIN, DIN; |
| | | font-weight: bold; |
| | | font-size: 22px; |
| | | color: #FFFFFF; |
| | | line-height: 20px; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .content { |
| | | height: 0; |
| | | flex: 1; |
| | | margin-top: 20px; |
| | | overflow-x: hidden; |
| | | overflow-y: auto; |
| | | } |
| | | |
| | | |
| | | .right-container { |
| | | transition: transform 0.3s ease-in-out; |
| | | position: relative; |
| | | |
| | | &.collapsed { |
| | | transform: translateX(100%); |
| | | } |
| | | } |
| | | |
| | | .collapse-btn { |
| | | position: absolute; |
| | | top: 50%; |
| | | right: 357px; |
| | | cursor: pointer; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | color: #fff; |
| | | z-index: 10; |
| | | transform: translateY(-50%); |
| | | |
| | | .arrow { |
| | | width: 24px; |
| | | height: 24px; |
| | | background: url('@/assets/images/dataCockpit/right-btn.png') center / 100% 100% no-repeat; |
| | | } |
| | | |
| | | &:hover { |
| | | background: rgba(40, 79, 227, 0.6); |
| | | } |
| | | } |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <div class="device-card" @click="emit('click', data)"> |
| | | <div class="header"> |
| | | <div class="title"> |
| | | <span class="name">{{ data.name }}</span> |
| | | |
| | | <span class="status"> |
| | | <span :class="data.statusType"> |
| | | |
| | | </span> |
| | | |
| | | <span> |
| | | {{ data.status }} |
| | | </span> |
| | | </span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="content"> |
| | | <div class="row"> |
| | | <div class="col"> |
| | | <span class="label">类型</span> |
| | | {{ data.type }} |
| | | </div> |
| | | <div class="col"> |
| | | <span class="label">厂商</span> |
| | | {{ data.manufacturer }} |
| | | </div> |
| | | </div> |
| | | <div class="row"> |
| | | <div class="col"> |
| | | <span class="label">方位角</span> |
| | | {{ data.azimuth }}° |
| | | </div> |
| | | <div class="col"> |
| | | <span class="label">俯仰角</span> |
| | | {{ data.pitch }}° |
| | | </div> |
| | | </div> |
| | | <div class="row"> |
| | | <div class="col"> |
| | | <span class="label">有效范围</span> |
| | | {{ data.range }} |
| | | </div> |
| | | <div class="col"> |
| | | <span class="label">设备型号</span> |
| | | {{ data.model }} |
| | | </div> |
| | | </div> |
| | | |
| | | <div> |
| | | <span class="label">部署位置</span> |
| | | <span class="value">{{ data.longitude }},{{ data.latitude }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | const props = defineProps({ |
| | | data: { |
| | | type: Object, |
| | | required: true, |
| | | default: () => ({}) |
| | | } |
| | | }) |
| | | |
| | | const emit = defineEmits(['click']) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .device-card { |
| | | margin-top: 10px; |
| | | padding: 10px; |
| | | color: #fff; |
| | | background: #191933; |
| | | border-radius: 6px 6px 6px 6px; |
| | | |
| | | &:first-child { |
| | | margin-top: 0; |
| | | } |
| | | |
| | | .header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | |
| | | .title { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | |
| | | .name { |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: bold; |
| | | font-size: 14px; |
| | | color: #FFFFFF; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | |
| | | .status { |
| | | padding: 0 10px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | height: 21px; |
| | | |
| | | background: #303041; |
| | | border-radius: 10px; |
| | | gap: 6px; |
| | | |
| | | span { |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: 400; |
| | | font-size: 10px; |
| | | color: #FFFFFF; |
| | | text-align: center; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | |
| | | |
| | | span.offline { |
| | | width: 4px; |
| | | height: 4px; |
| | | background: #939393; |
| | | border-radius: 50%; |
| | | } |
| | | |
| | | span.fault { |
| | | width: 4px; |
| | | height: 4px; |
| | | background: #FF4444; |
| | | border-radius: 50%; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .content { |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: #9E9EBA; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | |
| | | &>div { |
| | | margin-top: 10px; |
| | | line-height: 22px; |
| | | |
| | | span.label { |
| | | margin-right: 10px; |
| | | color: #D4D5D7; |
| | | } |
| | | } |
| | | |
| | | .row { |
| | | display: flex; |
| | | |
| | | &>div { |
| | | width: 0; |
| | | flex: 1; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <div class="drone-info-card" @click="emit('click', data)"> |
| | | <div class="title"> |
| | | <span class="name"> |
| | | {{ data.droneName }} |
| | | </span> |
| | | </div> |
| | | |
| | | <div class="rows"> |
| | | <div class="row"> |
| | | <span class="label">序列号</span> |
| | | <span class="value">{{ data.droneSerialNo }}</span> |
| | | </div> |
| | | |
| | | <div class="row"> |
| | | <span class="label">数据源</span> |
| | | <span class="value">{{ getDataSource(data) }}</span> |
| | | </div> |
| | | |
| | | <div class="row"> |
| | | <span class="label">信号频段</span> |
| | | <span class="value">{{ formatFrequency(data.signalFreqMhz) }}</span> |
| | | </div> |
| | | |
| | | <div class="row"> |
| | | <span class="label">发现时间</span> |
| | | <span class="value"> |
| | | <span class="date">{{ getAlarmDate(data.alarmTime) }}</span> |
| | | <span class="time">{{ getAlarmTime(data.alarmTime) }}</span> |
| | | </span> |
| | | </div> |
| | | |
| | | <div class="row"> |
| | | <span class="label">停留时间</span> |
| | | <span class="value">{{ data.stayDuration }}</span> |
| | | </div> |
| | | |
| | | <div class="row"> |
| | | <span class="label">反制方式</span> |
| | | <span class="value">{{ getCounterWayText(data.counterWay) }}</span> |
| | | </div> |
| | | |
| | | <div class="row"> |
| | | <span class="label">飞行状态</span> |
| | | <span class="value">{{ getFlightStatusText(data.flightStatus) }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | defineProps({ |
| | | data: { |
| | | type: Object, |
| | | required: true, |
| | | default: () => ({}) |
| | | } |
| | | }) |
| | | |
| | | const emit = defineEmits(['click']) |
| | | |
| | | const getDataSource = (item) => { |
| | | return item?.deviceName || item?.areaName || '-' |
| | | } |
| | | |
| | | const formatFrequency = (value) => { |
| | | if (value == null || value === '') return '-' |
| | | return `${value}MHZ` |
| | | } |
| | | |
| | | const getAlarmDate = (value) => { |
| | | if (!value) return '-' |
| | | const parts = value.replace('T', ' ').split(' ') |
| | | return parts[0] || value |
| | | } |
| | | |
| | | const getAlarmTime = (value) => { |
| | | if (!value) return '-' |
| | | const parts = value.replace('T', ' ').split(' ') |
| | | return parts[1] || value |
| | | } |
| | | |
| | | const getFlightStatusText = (value) => { |
| | | if (value === 1 || value === '1') return '侦测中' |
| | | if (value === 2 || value === '2') return '反制中' |
| | | return value || '-' |
| | | } |
| | | |
| | | const getCounterWayText = (value) => { |
| | | if (value === 1 || value === '1') return '信号干扰' |
| | | if (value === 2 || value === '2') return '诱导驱离' |
| | | if (value === 3 || value === '3') return '无' |
| | | return value || '-' |
| | | } |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .drone-info-card { |
| | | margin-top: 10px; |
| | | padding: 10px; |
| | | color: #fff; |
| | | background: #191933; |
| | | border-radius: 6px 6px 6px 6px; |
| | | |
| | | &:hover { |
| | | filter: brightness(1.05); |
| | | } |
| | | |
| | | .title { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | |
| | | .name { |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: bold; |
| | | font-size: 14px; |
| | | color: #FFFFFF; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | } |
| | | |
| | | .rows { |
| | | margin-top: 10px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: #9E9EBA; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | |
| | | .row { |
| | | line-height: 22px; |
| | | |
| | | .label { |
| | | margin-right: 10px; |
| | | color: #D4D5D7; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <div class="counter-card" @click="emit('click', data)"> |
| | | <!-- 头部 --> |
| | | <div class="header"> |
| | | <div class="title"> |
| | | <span class="name">{{ data.name }}</span> |
| | | |
| | | <span class="status"> |
| | | <span :class="getStatusType(data.status)"> |
| | | |
| | | </span> |
| | | |
| | | <span> |
| | | {{ getStatusText(data.status) }} |
| | | </span> |
| | | </span> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 字段区:两列 --> |
| | | <div class="content"> |
| | | <div class="row"> |
| | | <div class="col"> |
| | | <span class="label">类型</span> |
| | | {{ data.type }} |
| | | </div> |
| | | <div class="col"> |
| | | <span class="label">电量</span> |
| | | {{ data.batteryPct }}% |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="row"> |
| | | <div class="col"> |
| | | <span class="label">方位角</span> |
| | | {{ data.azimuth }}° |
| | | </div> |
| | | <div class="col"> |
| | | <span class="label">俯仰角</span> |
| | | {{ data.elevation }}° |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="row"> |
| | | <div class="col"> |
| | | <span class="label">状态</span> |
| | | {{ getStatusText(data.status) }} |
| | | </div> |
| | | <div class="col"> |
| | | <span class="label">厂商</span> |
| | | {{ data.manufacturer }} |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="row"> |
| | | <div class="col"> |
| | | <span class="label">侦测目标</span> |
| | | {{ data.detectTargetCnt }}台 |
| | | </div> |
| | | <div class="col"> |
| | | <span class="label">有效范围</span> |
| | | {{ data.effectiveRangeKm }}KM |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 底部按钮 --> |
| | | <div class="footer"> |
| | | <el-button color="#284FE3" type="primary" @click.stop="emit('history', data)">历史数据</el-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | defineProps({ |
| | | data: { |
| | | type: Object, |
| | | required: true, |
| | | default: () => ({}) |
| | | } |
| | | }) |
| | | |
| | | const emit = defineEmits(['history', 'click']) |
| | | |
| | | const getStatusText = (value) => { |
| | | if (value === 0 || value === '0') return '在线' |
| | | if (value === 1 || value === '1') return '离线' |
| | | if (value === 2 || value === '2') return '故障' |
| | | if (value === 3 || value === '3') return '报废' |
| | | return value || '-' |
| | | } |
| | | |
| | | const getStatusType = (value) => { |
| | | if (value === 0 || value === '0') return 'detecting' |
| | | if (value === 2 || value === '2') return 'jamming' |
| | | return '' |
| | | } |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .counter-card { |
| | | margin-top: 10px; |
| | | padding: 10px; |
| | | color: #fff; |
| | | background: #191933; |
| | | border-radius: 6px 6px 6px 6px; |
| | | |
| | | &:first-child { |
| | | margin-top: 0; |
| | | } |
| | | |
| | | .header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | |
| | | .title { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | |
| | | .name { |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: bold; |
| | | font-size: 14px; |
| | | color: #FFFFFF; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | |
| | | .status { |
| | | padding: 0 10px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | height: 21px; |
| | | |
| | | background: #303041; |
| | | border-radius: 10px; |
| | | gap: 6px; |
| | | |
| | | span { |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: 400; |
| | | font-size: 10px; |
| | | color: #FFFFFF; |
| | | text-align: center; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | |
| | | |
| | | span.detecting { |
| | | width: 4px; |
| | | height: 4px; |
| | | background: #18FF4A; |
| | | border-radius: 50%; |
| | | } |
| | | |
| | | span.jamming { |
| | | width: 4px; |
| | | height: 4px; |
| | | background: #FF4444; |
| | | border-radius: 50%; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .content { |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: #9E9EBA; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | |
| | | &>div { |
| | | margin-top: 10px; |
| | | line-height: 22px; |
| | | |
| | | span.label { |
| | | margin-right: 10px; |
| | | color: #D4D5D7; |
| | | } |
| | | } |
| | | |
| | | .row { |
| | | display: flex; |
| | | |
| | | &>div { |
| | | width: 0; |
| | | flex: 1; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .footer { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 10px; |
| | | margin-top: 10px; |
| | | |
| | | ::v-deep(.el-button) { |
| | | width: 96px; |
| | | height: 36px; |
| | | } |
| | | } |
| | | } |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <div class="drone-card"> |
| | | <div class="header"> |
| | | <div class="title"> |
| | | <span class="name">{{ data.droneName }}</span> |
| | | <span class="status"> |
| | | <span :class="getStatusType(data)"> |
| | | |
| | | </span> |
| | | |
| | | <span> |
| | | {{ getStatusText(data) }} |
| | | </span> |
| | | </span> |
| | | </div> |
| | | |
| | | <div class="actions"> |
| | | <span class="star" @click="emit('favorite')"> |
| | | {{ data.isFavorite ? '★' : '☆' }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="content"> |
| | | <div> |
| | | <span class="label">序列号</span> |
| | | {{ data.droneSerialNo }} |
| | | </div> |
| | | <div> |
| | | <span class="label">数据源</span> |
| | | {{ getDataSource(data) }} |
| | | </div> |
| | | <div> |
| | | <span class="label">经纬度</span> |
| | | {{ data.longitude }}, {{ data.latitude }} |
| | | </div> |
| | | <div class="row"> |
| | | <div class="col"> |
| | | <span class="label">信号频段</span> |
| | | {{ formatFrequency(data.signalFreqMhz) }} |
| | | </div> |
| | | <div class="col"> |
| | | <span class="label">发现时间</span> |
| | | {{ getAlarmTime(data.alarmTime) }} |
| | | </div> |
| | | </div> |
| | | <div class="row"> |
| | | <div class="col"> |
| | | <span class="label">飞行高度</span> |
| | | {{ formatHeight(data.flightHeightM) }} |
| | | </div> |
| | | <div class="col"> |
| | | <span class="label">飞行速度</span> |
| | | {{ formatSpeed(data.flightSpeedMs) }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="footer"> |
| | | <el-button class="general" color="#2B2B4C" @click="emit('signal')">信号干扰</el-button> |
| | | <el-button color="#284FE3" type="primary" @click="emit('counter')">诱导驱离</el-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | const props = defineProps({ |
| | | data: { |
| | | type: Object, |
| | | required: true, |
| | | default: () => ({}) |
| | | } |
| | | }) |
| | | |
| | | const emit = defineEmits(['signal', 'counter', 'favorite']) |
| | | |
| | | const getStatusText = (item) => { |
| | | const status = item?.flightStatus |
| | | if (status === 1 || status === '1') return '侦测中' |
| | | if (status === 2 || status === '2') return '反制中' |
| | | return status || '侦测中' |
| | | } |
| | | |
| | | const getStatusType = (item) => { |
| | | const statusText = getStatusText(item) |
| | | const counterWay = item?.counterWay |
| | | if (statusText.includes('反制') || counterWay === 2 || counterWay === '2' || counterWay === '诱导驱离') { |
| | | return 'countering' |
| | | } |
| | | return 'detecting' |
| | | } |
| | | |
| | | const getDataSource = (item) => { |
| | | return item?.deviceName || item?.areaName || '-' |
| | | } |
| | | |
| | | const formatFrequency = (value) => { |
| | | if (value == null || value === '') return '-' |
| | | return `${value}MHz` |
| | | } |
| | | |
| | | const getAlarmTime = (value) => { |
| | | if (!value) return '-' |
| | | const parts = value.replace('T', ' ').split(' ') |
| | | return parts[1] || value |
| | | } |
| | | |
| | | const formatHeight = (value) => { |
| | | if (value == null || value === '') return '-' |
| | | return `${value}m` |
| | | } |
| | | |
| | | const formatSpeed = (value) => { |
| | | if (value == null || value === '') return '-' |
| | | return `${value}m/s` |
| | | } |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .drone-card { |
| | | margin-top: 10px; |
| | | padding: 10px; |
| | | color: #fff; |
| | | background: #191933; |
| | | border-radius: 6px 6px 6px 6px; |
| | | |
| | | &:hover { |
| | | background: #29294D; |
| | | ; |
| | | } |
| | | |
| | | &:first-child { |
| | | margin-top: 0; |
| | | } |
| | | |
| | | .header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | |
| | | .title { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | |
| | | .name { |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: bold; |
| | | font-size: 14px; |
| | | color: #FFFFFF; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | |
| | | .status { |
| | | padding: 0 10px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | height: 21px; |
| | | |
| | | background: #303041; |
| | | border-radius: 10px; |
| | | gap: 6px; |
| | | |
| | | span { |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: 400; |
| | | font-size: 10px; |
| | | color: #FFFFFF; |
| | | text-align: center; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | |
| | | |
| | | span.detecting { |
| | | width: 4px; |
| | | height: 4px; |
| | | background: #18FF4A; |
| | | border-radius: 50%; |
| | | } |
| | | |
| | | span.countering { |
| | | width: 4px; |
| | | height: 4px; |
| | | background: #FF4444; |
| | | border-radius: 50%; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .actions { |
| | | display: flex; |
| | | gap: 8px; |
| | | font-size: 16px; |
| | | cursor: pointer; |
| | | } |
| | | } |
| | | |
| | | .content { |
| | | font-family: Source Han Sans CN, Source Han Sans CN; |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: #9E9EBA; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | |
| | | &>div { |
| | | margin-top: 10px; |
| | | line-height: 22px; |
| | | |
| | | span.label { |
| | | margin-right: 10px; |
| | | color: #D4D5D7; |
| | | } |
| | | } |
| | | |
| | | .row { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | } |
| | | } |
| | | |
| | | .footer { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 10px; |
| | | margin-top: 10px; |
| | | |
| | | ::v-deep(.el-button) { |
| | | width: 96px; |
| | | height: 36px; |
| | | |
| | | &.general.el-button { |
| | | border-radius: 4px 4px 4px 4px; |
| | | border: 1px solid rgba(255, 255, 255, 0.5); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | </style> |
| New file |
| | |
| | | <!-- |
| | | * @Author : yuan |
| | | * @Date : 2026-01-08 09:32:52 |
| | | * @LastEditors : yuan |
| | | * @LastEditTime : 2026-01-08 09:43:00 |
| | | * @FilePath : \applications\drone-command\src\views\dataCockpit\components\templateComponents\TitleTemplate.vue |
| | | * @Description : |
| | | * Copyright 2026 OBKoro1, All Rights Reserved. |
| | | * 2026-01-08 09:32:52 |
| | | --> |
| | | <template> |
| | | <div class="title-box"> |
| | | <div class="title"> |
| | | <slot></slot> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .title-box { |
| | | padding-left: 26px; |
| | | background: url('@/assets/images/dataCockpit/title-bg.png') center / 100% 100% no-repeat; |
| | | height: 34px; |
| | | |
| | | .title { |
| | | height: 25px; |
| | | line-height: 25px; |
| | | |
| | | font-family: PangMen; |
| | | font-weight: 400; |
| | | font-size: 18px; |
| | | color: #FFFFFF; |
| | | letter-spacing: 1px; |
| | | text-align: left; |
| | | font-style: normal; |
| | | text-transform: none; |
| | | } |
| | | } |
| | | </style> |
| New file |
| | |
| | | <!-- |
| | | * @Author : yuan |
| | | * @Date : 2026-01-06 16:35:50 |
| | | * @LastEditors : yuan |
| | | * @LastEditTime : 2026-01-09 16:34:39 |
| | | * @FilePath : \applications\drone-command\src\views\dataCockpit\index.vue |
| | | * @Description : |
| | | * Copyright 2026 OBKoro1, All Rights Reserved. |
| | | * 2026-01-06 16:35:50 |
| | | --> |
| | | <template> |
| | | <div class="page-container"> |
| | | <DeviceMapContainer |
| | | :online-devices="onlineDevices" |
| | | :left-collapsed="leftCollapsed" |
| | | container-id="data-cockpit-map" |
| | | /> |
| | | <LeftContainer class="left-container" v-model:activeId="leftActiveId" v-model:collapsed="leftCollapsed" /> |
| | | <RightContainer class="right-container" v-model:collapsed="rightCollapsed" v-model:onlineDevices="onlineDevices" /> |
| | | <CenterContainer @select-warning="onSelectWarning" @select-equipment="onSelectEquipment" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import DeviceMapContainer from '@/components/map-container/device-map-container.vue'; |
| | | import LeftContainer from './components/LeftContainer.vue'; |
| | | import RightContainer from './components/RightContainer.vue'; |
| | | import CenterContainer from './components/CenterContainer.vue'; |
| | | |
| | | const leftActiveId = ref(1); |
| | | const leftCollapsed = ref(false); |
| | | |
| | | const rightCollapsed = ref(false); |
| | | const onlineDevices = ref([]); |
| | | |
| | | const onSelectWarning = (type) => { |
| | | leftActiveId.value = type; |
| | | leftCollapsed.value = false; |
| | | }; |
| | | |
| | | const onSelectEquipment = () => { |
| | | rightCollapsed.value = false; |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .page-container { |
| | | position: relative; |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | |
| | | .left-container, |
| | | .right-container { |
| | | position: absolute; |
| | | top: 96px; |
| | | bottom: 0; |
| | | width: 401px; |
| | | z-index: 9; |
| | | backdrop-filter: blur(2px); |
| | | |
| | | ::v-deep(.wrapper) { |
| | | display: flex; |
| | | flex-direction: column; |
| | | position: absolute; |
| | | top: 17px; |
| | | bottom: 20px; |
| | | padding: 20px; |
| | | width: 300px; |
| | | background: rgba(0,0,0,0.3); |
| | | backdrop-filter: blur(16px); |
| | | border-radius: 10px 10px 10px 10px; |
| | | } |
| | | } |
| | | |
| | | .left-container { |
| | | top: 96px; |
| | | left: 0; |
| | | background: linear-gradient( 270deg, rgba(17,23,34,0) 0%, rgba(17,23,34,0.56) 50%, rgba(17,23,34,0.96) 100%); |
| | | |
| | | ::v-deep(.wrapper) { |
| | | left: 17px; |
| | | } |
| | | } |
| | | |
| | | .right-container { |
| | | top: 54px; |
| | | right: 0; |
| | | background: linear-gradient( 270deg, rgba(17,23,34,0.96) 0.16%, rgba(17,23,34,0.56) 57.57%, rgba(4,30,37,0) 100%); |
| | | |
| | | ::v-deep(.wrapper) { |
| | | top: 21px; |
| | | right: 17px; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | const viewInstance = shallowRef(null); |
| | | const homeViewer = shallowRef(null); |
| | | let tbJwdList = []; |
| | | const loading = ref(false); |
| | | const loading = ref(true); |
| | | const tableLoading = ref(true); |
| | | const tableData = ref([]); |
| | | const AlltableData = ref([]); |
| | |
| | | label: '新大屏', |
| | | value: 4, |
| | | }, |
| | | { |
| | | label: '无人机管控', |
| | | value: 5, |
| | | }, |
| | | ], |
| | | hide: true, |
| | | rules: [ |
| New file |
| | |
| | | <template> |
| | | <el-dialog v-model="visible" :title="titleEnum[dialogMode]" :close-on-click-modal="false" width="800px"> |
| | | <!-- 查看模式 --> |
| | | <div v-if="readonly" class="view-mode"> |
| | | <el-row :gutter="16"> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">登录账号:</span> |
| | | {{ formData.account }} |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">用户姓名:</span> |
| | | {{ formData.realName }} |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">手机号码:</span> |
| | | {{ formData.phone }} |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">电子邮箱:</span> |
| | | {{ formData.email }} |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">用户性别:</span> |
| | | {{ getSexLabel(formData.sex) }} |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">用户生日:</span> |
| | | {{ formData.birthday }} |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">所属角色:</span> |
| | | {{ formData.roleName }} |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">所属部门:</span> |
| | | {{ formData.deptName }} |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">用户编号:</span> |
| | | {{ formData.code }} |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">账号状态:</span> |
| | | {{ formData.statusName }} |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">到期时间:</span> |
| | | {{ formData.expireTime }} |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">次数限制:</span> |
| | | {{ formData.maxLoginNum }} |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">飞行开始时间:</span> |
| | | {{ formData.flightStartTime }} |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="view-item"> |
| | | <span class="label">飞行结束时间:</span> |
| | | {{ formData.flightEndTime }} |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | |
| | | <!-- 编辑/新增模式 --> |
| | | <el-form v-else ref="formRef" :model="formData" :rules="rules" label-width="110px"> |
| | | <el-divider content-position="left">基础信息</el-divider> |
| | | <el-row :gutter="16"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="登录账号" prop="account"> |
| | | <el-input v-model="formData.account" maxlength="50" placeholder="请输入" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12" v-if="dialogMode === 'add'"> |
| | | <el-form-item label="密码" prop="password"> |
| | | <el-input |
| | | v-model="formData.password" |
| | | type="password" |
| | | maxlength="50" |
| | | placeholder="请输入" |
| | | clearable |
| | | show-password |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12" v-if="dialogMode === 'add'"> |
| | | <el-form-item label="确认密码" prop="password2"> |
| | | <el-input |
| | | v-model="formData.password2" |
| | | type="password" |
| | | maxlength="50" |
| | | placeholder="请输入" |
| | | clearable |
| | | show-password |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-divider content-position="left">详细信息</el-divider> |
| | | <el-row :gutter="16"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="用户姓名" prop="realName"> |
| | | <el-input v-model="formData.realName" maxlength="50" placeholder="请输入" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="手机号码" prop="phone"> |
| | | <el-input v-model="formData.phone" maxlength="20" placeholder="请输入" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="电子邮箱" prop="email"> |
| | | <el-input v-model="formData.email" maxlength="50" placeholder="请输入" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="用户性别" prop="sex"> |
| | | <el-select v-model="formData.sex" placeholder="请选择" clearable> |
| | | <el-option label="男" :value="1" /> |
| | | <el-option label="女" :value="2" /> |
| | | <el-option label="未知" :value="3" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="用户生日" prop="birthday"> |
| | | <el-date-picker |
| | | v-model="formData.birthday" |
| | | type="datetime" |
| | | placeholder="请选择" |
| | | format="YYYY-MM-DD HH:mm:ss" |
| | | value-format="YYYY-MM-DD HH:mm:ss" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-divider content-position="left">职责信息</el-divider> |
| | | <el-row :gutter="16"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="用户编号" prop="code"> |
| | | <el-input v-model="formData.code" maxlength="50" placeholder="请输入" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="所属角色" prop="roleId"> |
| | | <el-tree-select |
| | | v-model="formData.roleId" |
| | | :data="roleTree" |
| | | :props="treeProps" |
| | | node-key="id" |
| | | check-strictly |
| | | clearable |
| | | placeholder="请选择" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="所属部门" prop="deptId"> |
| | | <el-tree-select |
| | | v-model="formData.deptId" |
| | | :data="deptTree" |
| | | :props="deptTreeSelectProps" |
| | | node-key="id" |
| | | check-strictly |
| | | clearable |
| | | placeholder="请选择" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-divider content-position="left">登录限制</el-divider> |
| | | <el-row :gutter="16"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="到期时间" prop="expireTime"> |
| | | <el-date-picker |
| | | v-model="formData.expireTime" |
| | | type="datetime" |
| | | placeholder="请选择" |
| | | format="YYYY-MM-DD HH:mm:ss" |
| | | value-format="YYYY-MM-DD HH:mm:ss" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="次数限制" prop="maxLoginNum"> |
| | | <el-input v-model="formData.maxLoginNum" placeholder="请输入" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="飞行开始时间" prop="flightStartTime"> |
| | | <el-time-picker |
| | | v-model="formData.flightStartTime" |
| | | placeholder="请选择" |
| | | format="HH:mm" |
| | | value-format="HH:mm" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="飞行结束时间" prop="flightEndTime"> |
| | | <el-time-picker |
| | | v-model="formData.flightEndTime" |
| | | placeholder="请选择" |
| | | format="HH:mm" |
| | | value-format="HH:mm" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | |
| | | <template #footer> |
| | | <el-button @click="handleCancel">{{ readonly ? '关闭' : '取消' }}</el-button> |
| | | <el-button v-if="!readonly" type="primary" :loading="submitting" :disabled="submitting" @click="handleSubmit"> |
| | | 确定 |
| | | </el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, onMounted, ref } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { getUser, add, update } from '@/api/system/user' |
| | | import { getDeptTree } from '@/api/system/dept' |
| | | import { getRoleTree } from '@/api/system/role' |
| | | import func from '@/utils/func' |
| | | |
| | | // 初始化表单 |
| | | const initForm = () => ({ |
| | | account: '', |
| | | password: '', |
| | | password2: '', |
| | | realName: '', |
| | | phone: '', |
| | | email: '', |
| | | sex: null, |
| | | birthday: '', |
| | | code: '', |
| | | roleId: '', |
| | | deptId: '', |
| | | expireTime: '', |
| | | maxLoginNum: '', |
| | | flightStartTime: '', |
| | | flightEndTime: '', |
| | | }) |
| | | |
| | | const treeProps = { |
| | | label: 'title', |
| | | children: 'children', |
| | | } |
| | | const deptTreeSelectProps = { |
| | | label: 'name', |
| | | value: 'id', |
| | | children: 'children', |
| | | } |
| | | |
| | | const emit = defineEmits(['success']) |
| | | const formRef = ref(null) |
| | | const formData = ref(initForm()) |
| | | const visible = defineModel() |
| | | const dialogMode = ref('add') |
| | | const submitting = ref(false) |
| | | const readonly = computed(() => dialogMode.value === 'view') |
| | | const titleEnum = ref({ edit: '编辑用户', view: '查看用户', add: '新增用户' }) |
| | | const roleTree = ref([]) |
| | | const deptTree = ref([]) |
| | | |
| | | // 密码验证 |
| | | const validatePass = (rule, value, callback) => { |
| | | if (value === '') { |
| | | callback(new Error('请输入密码')) |
| | | } else { |
| | | callback() |
| | | } |
| | | } |
| | | const validatePass2 = (rule, value, callback) => { |
| | | if (value === '') { |
| | | callback(new Error('请再次输入密码')) |
| | | } else if (value !== formData.value.password) { |
| | | callback(new Error('两次输入密码不一致!')) |
| | | } else { |
| | | callback() |
| | | } |
| | | } |
| | | |
| | | const rules = { |
| | | account: [{ required: true, message: '请输入登录账号', trigger: 'blur' }], |
| | | password: [{ required: true, validator: validatePass, trigger: 'blur' }], |
| | | password2: [{ required: true, validator: validatePass2, trigger: 'blur' }], |
| | | realName: [ |
| | | { required: true, message: '请输入用户姓名', trigger: 'blur' }, |
| | | { min: 2, max: 50, message: '姓名长度在2到50个字符', trigger: 'blur' }, |
| | | ], |
| | | roleId: [{ required: true, message: '请选择所属角色', trigger: 'change' }], |
| | | deptId: [{ required: true, message: '请选择所属部门', trigger: 'change' }], |
| | | } |
| | | |
| | | // 获取性别标签 |
| | | function getSexLabel(sex) { |
| | | const map = { 1: '男', 2: '女', 3: '未知' } |
| | | return map[sex] || '' |
| | | } |
| | | |
| | | // 获取角色树 |
| | | async function getRoleTreeData() { |
| | | const res = await getRoleTree('000000') |
| | | roleTree.value = res.data.data |
| | | } |
| | | |
| | | // 获取部门树 |
| | | async function getDeptTreeData() { |
| | | const res = await getDeptTree('000000') |
| | | deptTree.value = res.data.data |
| | | } |
| | | |
| | | // 关闭弹框 |
| | | function handleCancel() { |
| | | visible.value = false |
| | | } |
| | | |
| | | // 验证飞行时间 |
| | | function validateFlightTime() { |
| | | if (formData.value.flightStartTime && formData.value.flightEndTime) { |
| | | const [startHours, startMinutes] = formData.value.flightStartTime.split(':').map(Number) |
| | | const [endHours, endMinutes] = formData.value.flightEndTime.split(':').map(Number) |
| | | const startTotalMinutes = startHours * 60 + startMinutes |
| | | const endTotalMinutes = endHours * 60 + endMinutes |
| | | if (endTotalMinutes < startTotalMinutes) { |
| | | ElMessage.error('飞行结束时间不能小于飞行开始时间') |
| | | return false |
| | | } |
| | | } |
| | | return true |
| | | } |
| | | |
| | | // 提交新增/编辑 |
| | | async function handleSubmit() { |
| | | const isValid = await formRef.value?.validate().catch(() => false) |
| | | if (!isValid) return |
| | | if (!validateFlightTime()) return |
| | | |
| | | submitting.value = true |
| | | try { |
| | | const submitData = { ...formData.value } |
| | | submitData.roleId = func.toStr(submitData.roleId) |
| | | submitData.name = submitData.realName |
| | | |
| | | if (dialogMode.value === 'add') { |
| | | await add(submitData) |
| | | ElMessage.success('新增成功') |
| | | } else { |
| | | await update(submitData) |
| | | ElMessage.success('更新成功') |
| | | } |
| | | visible.value = false |
| | | emit('success') |
| | | } finally { |
| | | submitting.value = false |
| | | } |
| | | } |
| | | |
| | | // 加载详情 |
| | | async function loadDetail() { |
| | | if (!formData.value.id) return |
| | | const res = await getUser(formData.value.id) |
| | | const data = res?.data?.data ?? {} |
| | | // 处理 roleId |
| | | if (data.roleId) { |
| | | data.roleId = func.split(data.roleId)[0] || data.roleId |
| | | } |
| | | formData.value = data |
| | | } |
| | | |
| | | // 打开弹框 |
| | | async function open({ mode, row } = {}) { |
| | | dialogMode.value = mode || 'add' |
| | | if (dialogMode.value === 'add') { |
| | | formData.value = initForm() |
| | | } else { |
| | | formData.value = { id: row.id } |
| | | await loadDetail() |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getRoleTreeData() |
| | | getDeptTreeData() |
| | | }) |
| | | |
| | | defineExpose({ open }) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .view-mode { |
| | | .view-item { |
| | | padding: 10px 0; |
| | | .label { |
| | | color: #606266; |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | } |
| | | |
| | | :deep(.el-select) { |
| | | width: 100%; |
| | | } |
| | | |
| | | :deep(.el-date-editor) { |
| | | width: 100%; |
| | | } |
| | | |
| | | :deep(.el-tree-select) { |
| | | width: 100%; |
| | | } |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <basic-container> |
| | | <!-- 左侧部门树 --> |
| | | <el-row :gutter="16" class="user-container"> |
| | | <el-col :span="4"> |
| | | <div class="tree-box"> |
| | | <el-scrollbar> |
| | | <el-tree |
| | | :data="deptTree" |
| | | :props="deptTreeProps" |
| | | node-key="id" |
| | | :load="loadDeptTree" |
| | | lazy |
| | | @node-click="handleDeptNodeClick" |
| | | /> |
| | | </el-scrollbar> |
| | | </div> |
| | | </el-col> |
| | | <!-- 右侧列表区 --> |
| | | <el-col :span="20"> |
| | | <!-- 搜索表单 --> |
| | | <el-form ref="queryParamsRef" :model="searchParams"> |
| | | <el-row :gutter="16"> |
| | | <el-col :span="5"> |
| | | <el-form-item label="登录账号" prop="account"> |
| | | <el-input v-model="searchParams.account" placeholder="请输入" clearable @clear="handleSearch" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="5"> |
| | | <el-form-item label="用户姓名" prop="realName"> |
| | | <el-input v-model="searchParams.realName" placeholder="请输入" clearable @clear="handleSearch" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="4"> |
| | | <el-form-item> |
| | | <el-button @click="resetForm">重置</el-button> |
| | | <el-button type="primary" @click="handleSearch">查询</el-button> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | |
| | | <!-- 操作按钮 --> |
| | | <div class="btn-group"> |
| | | <el-button v-if="permission.user_add && !auditMode" type="primary" @click="openForm('add')">新增</el-button> |
| | | <el-button |
| | | v-if="permission.user_delete && !auditMode" |
| | | type="danger" |
| | | :disabled="!selectedIds.length" |
| | | @click="handleDelete()" |
| | | > |
| | | 删除 |
| | | </el-button> |
| | | <el-button |
| | | v-if="userInfo.role_name?.includes('admin') && auditMode" |
| | | type="success" |
| | | :disabled="!selectedIds.length" |
| | | @click="handleAuditPass" |
| | | > |
| | | 通过 |
| | | </el-button> |
| | | <el-button |
| | | v-if="userInfo.role_name?.includes('admin') && auditMode" |
| | | type="danger" |
| | | :disabled="!selectedIds.length" |
| | | @click="handleAuditRefuse" |
| | | > |
| | | 拒绝 |
| | | </el-button> |
| | | <el-button |
| | | v-if="userInfo.role_name?.includes('admin') && auditMode" |
| | | type="primary" |
| | | plain |
| | | @click="handleAuditBack" |
| | | > |
| | | 返回 |
| | | </el-button> |
| | | <el-button |
| | | v-if="permission.user_role && !auditMode" |
| | | type="info" |
| | | plain |
| | | :disabled="!selectedIds.length" |
| | | @click="handleGrant" |
| | | > |
| | | 角色配置 |
| | | </el-button> |
| | | <el-button |
| | | v-if="permission.user_reset && !auditMode" |
| | | type="info" |
| | | plain |
| | | :disabled="!selectedIds.length" |
| | | @click="handleReset" |
| | | > |
| | | 密码重置 |
| | | </el-button> |
| | | <el-button |
| | | v-if="userInfo.role_name?.includes('admin') && !auditMode" |
| | | type="info" |
| | | plain |
| | | :disabled="!selectedIds.length" |
| | | @click="handleLock" |
| | | > |
| | | 账号解封 |
| | | </el-button> |
| | | <el-button |
| | | v-if="userInfo.role_name?.includes('admin') && !auditMode" |
| | | type="success" |
| | | plain |
| | | @click="handleImport" |
| | | > |
| | | 导入 |
| | | </el-button> |
| | | <el-button |
| | | v-if="userInfo.role_name?.includes('admin') && !auditMode" |
| | | type="warning" |
| | | plain |
| | | @click="handleExport" |
| | | > |
| | | 导出 |
| | | </el-button> |
| | | </div> |
| | | |
| | | <!-- 用户列表 --> |
| | | <el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange"> |
| | | <el-table-column type="selection" width="55" /> |
| | | <el-table-column type="index" width="60" label="序号" /> |
| | | <el-table-column prop="account" label="登录账号" /> |
| | | <el-table-column prop="realName" label="用户姓名" /> |
| | | <el-table-column prop="roleName" label="所属角色"> |
| | | <template v-slot="{ row }"> |
| | | <el-tag v-if="row.roleName">{{ row.roleName }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="deptName" label="所属部门"> |
| | | <template v-slot="{ row }"> |
| | | <el-tag v-if="row.deptName">{{ row.deptName }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="phone" label="手机号码" /> |
| | | <el-table-column label="操作" width="180"> |
| | | <template v-slot="{ row }"> |
| | | <el-link v-if="permission.user_view" @click="openForm('view', row)" type="primary">查看</el-link> |
| | | <el-link v-if="permission.user_edit && !auditMode" @click="openForm('edit', row)" type="warning"> |
| | | 编辑 |
| | | </el-link> |
| | | <el-link v-if="permission.user_delete && !auditMode" @click="handleDelete(row)" type="danger"> |
| | | 删除 |
| | | </el-link> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <!-- 分页 --> |
| | | <div> |
| | | <el-pagination |
| | | v-model:current-page="searchParams.current" |
| | | v-model:page-size="searchParams.size" |
| | | :total="total" |
| | | @change="getList" |
| | | /> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- 用户表单弹框 --> |
| | | <FormDiaLog v-if="dialogVisible" v-model="dialogVisible" ref="dialogRef" @success="getList" /> |
| | | |
| | | <!-- 角色配置弹框 --> |
| | | <el-dialog title="用户角色配置" append-to-body v-model="roleBox" width="345px"> |
| | | <el-tree |
| | | :data="roleGrantList" |
| | | show-checkbox |
| | | check-strictly |
| | | default-expand-all |
| | | node-key="id" |
| | | ref="treeRoleRef" |
| | | :default-checked-keys="roleTreeObj" |
| | | :props="roleProps" |
| | | ></el-tree> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button @click="roleBox = false">取 消</el-button> |
| | | <el-button type="primary" @click="submitRole">确 定</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- 导入弹框 --> |
| | | <el-dialog title="用户数据导入" append-to-body v-model="excelBox" width="555px"> |
| | | <el-form :model="excelForm" label-width="100px"> |
| | | <el-form-item label="数据覆盖"> |
| | | <el-switch v-model="excelForm.isCovered" :active-value="1" :inactive-value="0" /> |
| | | </el-form-item> |
| | | <el-form-item label="模板上传"> |
| | | <el-upload |
| | | :action="uploadAction" |
| | | :on-success="handleUploadSuccess" |
| | | accept=".xls,.xlsx" |
| | | :show-file-list="false" |
| | | > |
| | | <el-button type="primary">点击上传</el-button> |
| | | </el-upload> |
| | | <div class="upload-tip">请上传 .xls,.xlsx 标准格式文件</div> |
| | | </el-form-item> |
| | | <el-form-item label="模板下载"> |
| | | <el-button type="primary" @click="handleTemplate">点击下载</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </el-dialog> |
| | | </basic-container> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, nextTick, onMounted, ref } from 'vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { useStore } from 'vuex' |
| | | import FormDiaLog from './FormDiaLog.vue' |
| | | import { getList as getListApi, remove, grant, resetPassword, unlock, auditPass, auditRefuse } from '@/api/system/user' |
| | | import { exportBlob } from '@/api/common' |
| | | import { getDeptTree, getDeptLazyTree } from '@/api/system/dept' |
| | | import { getRoleTree } from '@/api/system/role' |
| | | import website from '@/config/website' |
| | | import { getToken } from '@/utils/auth' |
| | | import { downloadXls } from '@/utils/util' |
| | | import NProgress from 'nprogress' |
| | | import 'nprogress/nprogress.css' |
| | | import func from '@/utils/func' |
| | | |
| | | const store = useStore() |
| | | const userInfo = computed(() => store.getters.userInfo) |
| | | const permission = computed(() => store.getters.permission) |
| | | |
| | | // 初始化搜索参数 |
| | | const initSearchParams = () => ({ |
| | | account: '', |
| | | realName: '', |
| | | current: 1, |
| | | size: 10, |
| | | }) |
| | | |
| | | const searchParams = ref(initSearchParams()) |
| | | const total = ref(0) |
| | | const loading = ref(false) |
| | | const list = ref([]) |
| | | const selectedIds = ref([]) |
| | | const selectionList = ref([]) |
| | | const queryParamsRef = ref(null) |
| | | const dialogRef = ref(null) |
| | | const dialogVisible = ref(false) |
| | | const auditMode = ref(false) |
| | | const treeDeptId = ref('') |
| | | |
| | | // 部门树 |
| | | const deptTree = ref([]) |
| | | const deptTreeProps = { |
| | | label: 'title', |
| | | children: 'children', |
| | | isLeaf: 'leaf', |
| | | } |
| | | |
| | | // 角色配置 |
| | | const roleBox = ref(false) |
| | | const roleGrantList = ref([]) |
| | | const roleTreeObj = ref([]) |
| | | const treeRoleRef = ref(null) |
| | | const roleProps = { |
| | | label: 'title', |
| | | value: 'key', |
| | | } |
| | | |
| | | // 导入相关 |
| | | const excelBox = ref(false) |
| | | const excelForm = ref({ |
| | | isCovered: 0, |
| | | }) |
| | | const uploadAction = computed(() => `/blade-system/user/import-user?isCovered=${excelForm.value.isCovered}`) |
| | | |
| | | provide('treeDeptId', treeDeptId) |
| | | |
| | | // 加载部门树节点 |
| | | async function loadDeptTree(node, resolve) { |
| | | const parentId = node.level === 0 ? 0 : node.data.id |
| | | const param = { |
| | | parentId: parentId, |
| | | level: node.level, |
| | | } |
| | | const res = await getDeptLazyTree(param) |
| | | resolve( |
| | | res.data.data.map(item => ({ |
| | | ...item, |
| | | leaf: !item.hasChildren, |
| | | })) |
| | | ) |
| | | } |
| | | |
| | | // 部门节点点击 |
| | | function handleDeptNodeClick(data) { |
| | | treeDeptId.value = data.id |
| | | searchParams.value.current = 1 |
| | | getList() |
| | | } |
| | | |
| | | // 获取列表 |
| | | async function getList() { |
| | | loading.value = true |
| | | try { |
| | | const res = await getListApi( |
| | | searchParams.value.current, |
| | | searchParams.value.size, |
| | | { |
| | | account: searchParams.value.account, |
| | | realName: searchParams.value.realName, |
| | | status: auditMode.value ? 0 : 1, |
| | | }, |
| | | treeDeptId.value |
| | | ) |
| | | list.value = res?.data?.data?.records ?? [] |
| | | total.value = res?.data?.data?.total ?? 0 |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | // 查询 |
| | | function handleSearch() { |
| | | searchParams.value.current = 1 |
| | | getList() |
| | | } |
| | | |
| | | // 重置查询 |
| | | function resetForm() { |
| | | queryParamsRef.value?.resetFields() |
| | | searchParams.value = initSearchParams() |
| | | treeDeptId.value = '' |
| | | getList() |
| | | } |
| | | |
| | | // 删除 |
| | | async function handleDelete(row) { |
| | | const tips = row ? '该条' : '选中的项' |
| | | await ElMessageBox.confirm(`确认删除${tips}吗?`, '提示', { type: 'warning' }) |
| | | const ids = row ? row.id : selectedIds.value.join(',') |
| | | await remove(ids) |
| | | ElMessage.success('删除成功') |
| | | selectedIds.value = [] |
| | | getList() |
| | | } |
| | | |
| | | // 勾选值设置 |
| | | function handleSelectionChange(rows) { |
| | | selectionList.value = rows |
| | | selectedIds.value = rows.map(item => item.id) |
| | | } |
| | | |
| | | // 角色配置 |
| | | async function handleGrant() { |
| | | if (selectedIds.value.length === 0) { |
| | | ElMessage.warning('请选择至少一条数据') |
| | | return |
| | | } |
| | | roleTreeObj.value = [] |
| | | if (selectionList.value.length === 1) { |
| | | roleTreeObj.value = selectionList.value[0].roleId?.split(',') || [] |
| | | } |
| | | const res = await getRoleTree() |
| | | roleGrantList.value = res.data.data |
| | | roleBox.value = true |
| | | } |
| | | |
| | | // 提交角色配置 |
| | | async function submitRole() { |
| | | const roleList = treeRoleRef.value.getCheckedKeys().join(',') |
| | | await grant(selectedIds.value.join(','), roleList) |
| | | roleBox.value = false |
| | | ElMessage.success('操作成功') |
| | | getList() |
| | | } |
| | | |
| | | // 密码重置 |
| | | async function handleReset() { |
| | | if (selectedIds.value.length === 0) { |
| | | ElMessage.warning('请选择至少一条数据') |
| | | return |
| | | } |
| | | await ElMessageBox.confirm('确定将选择账号密码重置为123456?', '提示', { type: 'warning' }) |
| | | await resetPassword(selectedIds.value.join(',')) |
| | | ElMessage.success('操作成功') |
| | | } |
| | | |
| | | // 账号解封 |
| | | async function handleLock() { |
| | | if (selectedIds.value.length === 0) { |
| | | ElMessage.warning('请选择至少一条数据') |
| | | return |
| | | } |
| | | await ElMessageBox.confirm('确定将选择账号解封?', '提示', { type: 'warning' }) |
| | | await unlock(selectedIds.value.join(',')) |
| | | ElMessage.success('操作成功') |
| | | } |
| | | |
| | | // 审核通过 |
| | | async function handleAuditPass() { |
| | | if (selectedIds.value.length === 0) { |
| | | ElMessage.warning('请选择至少一条数据') |
| | | return |
| | | } |
| | | await ElMessageBox.confirm('确定将选择数据通过审核?', '提示', { type: 'warning' }) |
| | | await auditPass(selectedIds.value.join(',')) |
| | | ElMessage.success('操作成功') |
| | | getList() |
| | | } |
| | | |
| | | // 审核拒绝 |
| | | async function handleAuditRefuse() { |
| | | if (selectedIds.value.length === 0) { |
| | | ElMessage.warning('请选择至少一条数据') |
| | | return |
| | | } |
| | | await ElMessageBox.confirm('确定将选择数据拒绝审核?', '提示', { type: 'warning' }) |
| | | await auditRefuse(selectedIds.value.join(',')) |
| | | ElMessage.success('操作成功') |
| | | getList() |
| | | } |
| | | |
| | | // 返回正常模式 |
| | | function handleAuditBack() { |
| | | auditMode.value = false |
| | | getList() |
| | | } |
| | | |
| | | // 导入 |
| | | function handleImport() { |
| | | excelBox.value = true |
| | | } |
| | | |
| | | // 上传成功 |
| | | function handleUploadSuccess() { |
| | | excelBox.value = false |
| | | getList() |
| | | ElMessage.success('导入成功') |
| | | } |
| | | |
| | | // 导出 |
| | | function handleExport() { |
| | | const account = func.toStr(searchParams.value.account) |
| | | const realName = func.toStr(searchParams.value.realName) |
| | | ElMessageBox.confirm('是否导出用户数据?', '提示', { type: 'warning' }).then(() => { |
| | | NProgress.start() |
| | | exportBlob( |
| | | `/blade-system/user/export-user?${website.tokenHeader}=${getToken()}&account=${account}&realName=${realName}` |
| | | ).then(res => { |
| | | downloadXls(res.data, `用户数据表${new Date().toISOString().slice(0, 10)}.xlsx`) |
| | | NProgress.done() |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | // 下载模板 |
| | | function handleTemplate() { |
| | | exportBlob(`/blade-system/user/export-template?${website.tokenHeader}=${getToken()}`).then(res => { |
| | | downloadXls(res.data, '用户数据模板.xlsx') |
| | | }) |
| | | } |
| | | |
| | | // 新增/编辑/查看 弹框 |
| | | function openForm(mode, row) { |
| | | dialogVisible.value = true |
| | | nextTick(() => { |
| | | dialogRef.value?.open({ mode, row }) |
| | | }) |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getList() |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .user-container { |
| | | height: 100%; |
| | | } |
| | | |
| | | .tree-box { |
| | | height: 100%; |
| | | padding: 10px; |
| | | border-radius: 5px; |
| | | background-color: #fff; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); |
| | | |
| | | :deep(.el-scrollbar) { |
| | | height: 100%; |
| | | } |
| | | } |
| | | |
| | | .btn-group { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .upload-tip { |
| | | color: #999; |
| | | font-size: 12px; |
| | | margin-top: 8px; |
| | | } |
| | | </style> |
| | |
| | | <template> |
| | | <wel-container> |
| | | <div class="workbench" v-if="display"> |
| | | <div class="workleft"> |
| | | <!-- 设备统计 --> |
| | | <statistics></statistics> |
| | | <!-- 综合统计分析 --> |
| | | <div class="comprehensiveCon"> |
| | | <div class="comprehensive"> |
| | | <div> |
| | | |
| | | <div class="center"> |
| | | <div class="centerLeft"> |
| | | <!-- 工单统计 --> |
| | | <proportionStatic></proportionStatic> |
| | | <!-- 飞行统计 --> |
| | | <flightStatistics></flightStatistics> |
| | | |
| | | </div> |
| | | <div class="centerRight"> |
| | | <!-- 机巢工单数量排名(件) --> |
| | | <flyratio></flyratio> |
| | | <!-- 任务成果 --> |
| | | <taskOutcome></taskOutcome> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="workright"> |
| | | <Bocklog></Bocklog> |
| | | <CalenBox></CalenBox> |
| | | </div> |
| | | </div> |
| | | </wel-container> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import taskOutcome from './components/taskOutcome.vue' |
| | | import flightStatistics from './components/flightStatistics.vue' |
| | | import proportionStatic from './components/proportionStatic.vue' |
| | | import flyratio from './components/flyratio.vue' |
| | | import Bocklog from './components/backlog.vue' |
| | | import CalenBox from './components/calendarBox.vue' |
| | | import * as echarts from 'echarts' |
| | | import useEchartsResize from '@/hooks/useEchartsResize' |
| | | import { mapGetters } from 'vuex' |
| | | import { getJobEventByStatus, getJobEventTotal, getFly, getFlyTime } from '@/api/home/index' |
| | | import overviewImg1 from '@/assets/images/workbench/tc1.png' |
| | | |
| | | import fy1 from '@/assets/images/workbench/fy1.png' |
| | | |
| | | import statistics from './components/statistics.vue' |
| | | import { ElMessage } from 'element-plus' |
| | | let checked = ref('CURRENT_YEAR') |
| | | const display = ref(false) |
| | | let timeListStr = ['本周', '本月', '本年'] |
| | | let timeListEnum = ['CURRENT_WEEK', 'CURRENT_MONTH', 'CURRENT_YEAR'] |
| | | const params = ref({ |
| | | date_enum: 'CURRENT_YEAR', |
| | | device_sn: '', |
| | | end_date: undefined, |
| | | start_date: undefined, |
| | | }) |
| | | const dateSelect = ref('CURRENT_YEAR') |
| | | let timeClick = (item, index) => { |
| | | checked.value = item |
| | | params.value.date_enum = item |
| | | dateSelect.value = item |
| | | |
| | | |
| | | |
| | | } |
| | | const refresh = () => { |
| | | params.value.date_enum = 'CURRENT_YEAR' |
| | | checked.value = 'CURRENT_YEAR' |
| | | dateSelect.value = 'CURRENT_YEAR' |
| | | |
| | | |
| | | } |
| | | // 跳转 |
| | | const jumpshebei = () => { |
| | | ElMessage.warning('加急开发中...') |
| | | } |
| | | |
| | | const eventTotal = ref(0) |
| | | const data = ref([]) |
| | | |
| | | onMounted(() => { |
| | | getJobEventTotal().then(res => { |
| | | eventTotal.value = res?.data?.data || 0 |
| | | }) |
| | | setTimeout(() => { |
| | | display.value = true |
| | | },100) |
| | | }) |
| | | </script> |
| | | |
| | | <style> |
| | | .el-font-size { |
| | | font-size: 14px; |
| | | } |
| | | </style> |
| | | <style scoped lang="scss"> |
| | | .workbench { |
| | | height: 100%; |
| | | flex: 1; |
| | | // padding: 0px 20px 0 10px; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | } |
| | | <style lang="scss" scoped> |
| | | |
| | | .workleft { |
| | | width: 68%; |
| | | margin-right: 10px; |
| | | height: 100%; |
| | | |
| | | .comprehensiveCon { |
| | | // background: #ffffff !important; |
| | | height: pxToVh(776); |
| | | border-radius: 8px 8px 8px 8px; |
| | | |
| | | .comprehensive { |
| | | |
| | | // padding: 14px 14px 0 21px; |
| | | |
| | | .title { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | |
| | | .name { |
| | | display: flex; |
| | | align-items: center; |
| | | font-weight: bold; |
| | | font-size: 16px; |
| | | color: #363636; |
| | | font-family: 'Source Han Sans CN'; |
| | | |
| | | span { |
| | | margin-right: 4px; |
| | | } |
| | | |
| | | img { |
| | | cursor: pointer; |
| | | } |
| | | } |
| | | |
| | | .arrow { |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .time-card { |
| | | text-align: center; |
| | | // height: 30px; |
| | | height: pxToVh(30); |
| | | background: #ffffff; |
| | | border-radius: 4px 0px 0px 4px; |
| | | border: 1px solid #e5e5e5; |
| | | font-weight: 400; |
| | | font-size: 14px; |
| | | color: #7c8091; |
| | | display: flex; |
| | | width: 282px; |
| | | margin-left: 15px; |
| | | |
| | | .card-item { |
| | | width: 94px; |
| | | height: 100%; |
| | | line-height: 28px; |
| | | cursor: pointer; |
| | | font-family: 'Source Han Sans CN'; |
| | | font-weight: 400; |
| | | font-size: 14px; |
| | | color: #7c8091; |
| | | } |
| | | |
| | | .card-item:first-child { |
| | | border-right: 1px solid #e5e5e5; |
| | | } |
| | | |
| | | .card-item:nth-child(2) { |
| | | border-right: 1px solid #e5e5e5; |
| | | } |
| | | |
| | | .card-item.active { |
| | | color: #1441ff; |
| | | border: 1px solid #1c5cff; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 工、单 |
| | | .center { |
| | | display: flex; |
| | | .centerLeft { |
| | | width: 50%; |
| | | |
| | | |
| | | } |
| | | |
| | | .centerRight { |
| | | width: 50%; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .workright { |
| | | width: 0; |
| | | flex: 1; |
| | | height: 100%; |
| | | // display: flex; |
| | | // flex-direction: column; |
| | | } |
| | | </style> |
| | | </style> |