Merge branch 'master' of http://139.196.74.78:10010/r/jagzwxm/ja_web
25 files modified
6 files added
7 files deleted
| | |
| | | }, |
| | | "dependencies": { |
| | | "@amap/amap-jsapi-loader": "catalog:", |
| | | "@ztzf/apis": "workspace:*", |
| | | "@ztzf/components": "workspace:*", |
| | | "@ztzf/constants": "workspace:*", |
| | | "@ztzf/hooks": "workspace:*", |
| | | "@ztzf/utils": "workspace:*", |
| | | "@dvgis/dc-sdk": "catalog:", |
| | | "@element-plus/icons-vue": "catalog:", |
| | | "@saber/nf-design-base-elp": "catalog:", |
| | |
| | | "@saber/nf-form-elp": "catalog:", |
| | | "@smallwei/avue": "catalog:", |
| | | "@turf/turf": "catalog:", |
| | | "@ztzf/apis": "workspace:*", |
| | | "@ztzf/components": "workspace:*", |
| | | "@ztzf/constants": "workspace:*", |
| | | "@ztzf/hooks": "workspace:*", |
| | | "@ztzf/utils": "workspace:*", |
| | | "animate.css": "catalog:", |
| | | "avue-plugin-ueditor": "catalog:", |
| | | "axios": "catalog:", |
| | |
| | | "decimal.js": "catalog:", |
| | | "disable-devtool": "catalog:", |
| | | "echarts": "catalog:", |
| | | "echarts-gl": "^2.0.9", |
| | | "element-plus": "catalog:", |
| | | "eventemitter3": "catalog:", |
| | | "highlight.js": "catalog:", |
| New file |
| | |
| | | <svg width="365" height="34" viewBox="0 0 365 34" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| | | <g id="Group 2136636843"> |
| | | <g id="Group 1321318807"> |
| | | <g id="Vector 482" filter="url(#filter0_f_62_4305)"> |
| | | <path d="M365 15.5H167.292L150.51 31.5H12.5863L7.34204 26.5002" stroke="url(#paint0_linear_62_4305)" stroke-width="1.6"/> |
| | | </g> |
| | | <g id="Rectangle 346241148" filter="url(#filter1_f_62_4305)"> |
| | | <path d="M24.1237 14.5H145.79L139.497 23.5H17.8306L24.1237 14.5Z" fill="url(#paint1_linear_62_4305)"/> |
| | | </g> |
| | | <g id="Vector 485" filter="url(#filter2_f_62_4305)"> |
| | | <path d="M6.29321 8H8.39091V27H6.4756H6.29321V8Z" fill="url(#paint2_linear_62_4305)"/> |
| | | </g> |
| | | <g id="Vector 483" filter="url(#filter3_ddf_62_4305)"> |
| | | <path d="M8.39078 34L1.04883 27V30.5L4.71981 34H8.39078Z" fill="white"/> |
| | | </g> |
| | | <g id="Vector 484" opacity="0.3" filter="url(#filter4_f_62_4305)"> |
| | | <path d="M0 0H31.4655V2H2.24754H0V0Z" fill="url(#paint3_linear_62_4305)"/> |
| | | </g> |
| | | <g id="Union" filter="url(#filter5_dd_62_4305)"> |
| | | <path d="M365 16H348.218V14H365V16Z" fill="white"/> |
| | | </g> |
| | | <path id="Union_2" d="M296.825 24H293.678V21H296.825V24Z" fill="#686B8D"/> |
| | | <path id="Union_3" d="M316.753 24H313.606V21H316.753V24Z" fill="#686B8D"/> |
| | | </g> |
| | | <g id="Group 1321318808"> |
| | | <g id="Rectangle 346241141" filter="url(#filter6_f_62_4305)"> |
| | | <path d="M174.109 20H182.5L173.06 30H164.669L174.109 20Z" fill="#0041FF"/> |
| | | </g> |
| | | <g id="Rectangle 346241142" filter="url(#filter7_f_62_4305)"> |
| | | <path d="M188.793 20H197.184L187.744 30H179.353L188.793 20Z" fill="#0041FF" fill-opacity="0.7"/> |
| | | </g> |
| | | <g id="Rectangle 346241143" filter="url(#filter8_f_62_4305)"> |
| | | <path d="M203.477 20H211.868L202.428 30H194.037L203.477 20Z" fill="#0041FF" fill-opacity="0.3"/> |
| | | </g> |
| | | <g id="Rectangle 346241144" filter="url(#filter9_f_62_4305)"> |
| | | <path d="M218.161 20H226.552L217.112 30H208.721L218.161 20Z" fill="#0041FF" fill-opacity="0.16"/> |
| | | </g> |
| | | </g> |
| | | </g> |
| | | <defs> |
| | | <filter id="filter0_f_62_4305" x="5.79004" y="13.7" width="360.21" height="19.6001" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
| | | <feFlood flood-opacity="0" result="BackgroundImageFix"/> |
| | | <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/> |
| | | <feGaussianBlur stdDeviation="0.5" result="effect1_foregroundBlur_62_4305"/> |
| | | </filter> |
| | | <filter id="filter1_f_62_4305" x="16.8306" y="13.5" width="129.96" height="11" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
| | | <feFlood flood-opacity="0" result="BackgroundImageFix"/> |
| | | <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/> |
| | | <feGaussianBlur stdDeviation="0.5" result="effect1_foregroundBlur_62_4305"/> |
| | | </filter> |
| | | <filter id="filter2_f_62_4305" x="5.29321" y="7" width="4.09766" height="21" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
| | | <feFlood flood-opacity="0" result="BackgroundImageFix"/> |
| | | <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/> |
| | | <feGaussianBlur stdDeviation="0.5" result="effect1_foregroundBlur_62_4305"/> |
| | | </filter> |
| | | <filter id="filter3_ddf_62_4305" x="-0.951172" y="25" width="11.342" height="11" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
| | | <feFlood flood-opacity="0" result="BackgroundImageFix"/> |
| | | <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="1"/> |
| | | <feComposite in2="hardAlpha" operator="out"/> |
| | | <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.4 0 0 0 0 1 0 0 0 1 0"/> |
| | | <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_62_4305"/> |
| | | <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="1"/> |
| | | <feComposite in2="hardAlpha" operator="out"/> |
| | | <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.4 0 0 0 0 1 0 0 0 1 0"/> |
| | | <feBlend mode="normal" in2="effect1_dropShadow_62_4305" result="effect2_dropShadow_62_4305"/> |
| | | <feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_62_4305" result="shape"/> |
| | | <feGaussianBlur stdDeviation="0.05" result="effect3_foregroundBlur_62_4305"/> |
| | | </filter> |
| | | <filter id="filter4_f_62_4305" x="-0.1" y="-0.1" width="31.6656" height="2.2" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
| | | <feFlood flood-opacity="0" result="BackgroundImageFix"/> |
| | | <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/> |
| | | <feGaussianBlur stdDeviation="0.05" result="effect1_foregroundBlur_62_4305"/> |
| | | </filter> |
| | | <filter id="filter5_dd_62_4305" x="342.218" y="8" width="28.7815" height="14" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
| | | <feFlood flood-opacity="0" result="BackgroundImageFix"/> |
| | | <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="3"/> |
| | | <feComposite in2="hardAlpha" operator="out"/> |
| | | <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.4 0 0 0 0 1 0 0 0 1 0"/> |
| | | <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_62_4305"/> |
| | | <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="3"/> |
| | | <feComposite in2="hardAlpha" operator="out"/> |
| | | <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.4 0 0 0 0 1 0 0 0 1 0"/> |
| | | <feBlend mode="normal" in2="effect1_dropShadow_62_4305" result="effect2_dropShadow_62_4305"/> |
| | | <feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_62_4305" result="shape"/> |
| | | </filter> |
| | | <filter id="filter6_f_62_4305" x="163.669" y="19" width="19.8306" height="12" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
| | | <feFlood flood-opacity="0" result="BackgroundImageFix"/> |
| | | <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/> |
| | | <feGaussianBlur stdDeviation="0.5" result="effect1_foregroundBlur_62_4305"/> |
| | | </filter> |
| | | <filter id="filter7_f_62_4305" x="178.353" y="19" width="19.8306" height="12" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
| | | <feFlood flood-opacity="0" result="BackgroundImageFix"/> |
| | | <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/> |
| | | <feGaussianBlur stdDeviation="0.5" result="effect1_foregroundBlur_62_4305"/> |
| | | </filter> |
| | | <filter id="filter8_f_62_4305" x="193.037" y="19" width="19.8306" height="12" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
| | | <feFlood flood-opacity="0" result="BackgroundImageFix"/> |
| | | <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/> |
| | | <feGaussianBlur stdDeviation="0.5" result="effect1_foregroundBlur_62_4305"/> |
| | | </filter> |
| | | <filter id="filter9_f_62_4305" x="207.721" y="19" width="19.8306" height="12" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
| | | <feFlood flood-opacity="0" result="BackgroundImageFix"/> |
| | | <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/> |
| | | <feGaussianBlur stdDeviation="0.5" result="effect1_foregroundBlur_62_4305"/> |
| | | </filter> |
| | | <linearGradient id="paint0_linear_62_4305" x1="17.8305" y1="23.5" x2="428.456" y2="23.5" gradientUnits="userSpaceOnUse"> |
| | | <stop stop-color="#89A1FF"/> |
| | | <stop offset="0.159605" stop-color="#D9DFFF"/> |
| | | <stop offset="1" stop-color="#3C4986" stop-opacity="0"/> |
| | | </linearGradient> |
| | | <linearGradient id="paint1_linear_62_4305" x1="17.8306" y1="18.5" x2="139.497" y2="18.5" gradientUnits="userSpaceOnUse"> |
| | | <stop stop-color="#002AA7"/> |
| | | <stop offset="1" stop-color="#0041FF" stop-opacity="0"/> |
| | | </linearGradient> |
| | | <linearGradient id="paint2_linear_62_4305" x1="7.34206" y1="8" x2="7.34206" y2="27" gradientUnits="userSpaceOnUse"> |
| | | <stop stop-color="#4B598C" stop-opacity="0"/> |
| | | <stop offset="1" stop-color="#8299F2"/> |
| | | </linearGradient> |
| | | <linearGradient id="paint3_linear_62_4305" x1="31.4655" y1="1" x2="0" y2="1" gradientUnits="userSpaceOnUse"> |
| | | <stop stop-color="#999999" stop-opacity="0"/> |
| | | <stop offset="1" stop-color="white"/> |
| | | </linearGradient> |
| | | </defs> |
| | | </svg> |
| New file |
| | |
| | | <svg width="618" height="34" viewBox="0 0 618 34" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| | | <g id="Group 2136636843"> |
| | | <g id="Group 1321318807"> |
| | | <g id="Vector 482" filter="url(#filter0_f_63_4945)"> |
| | | <path d="M617.49 15.5H162.658L146.342 31.5H12.2377L7.13867 26.5002" stroke="url(#paint0_linear_63_4945)" stroke-width="1.6"/> |
| | | </g> |
| | | <g id="Rectangle 346241148" filter="url(#filter1_f_63_4945)"> |
| | | <path d="M23.4555 14.5H141.753L135.634 23.5H17.3367L23.4555 14.5Z" fill="url(#paint1_linear_63_4945)"/> |
| | | </g> |
| | | <g id="Vector 485" filter="url(#filter2_f_63_4945)"> |
| | | <path d="M6.1189 8H8.1585V27H6.29623H6.1189V8Z" fill="url(#paint2_linear_63_4945)"/> |
| | | </g> |
| | | <g id="Vector 483" filter="url(#filter3_ddf_63_4945)"> |
| | | <path d="M8.15839 34L1.01978 27V30.5L4.58908 34H8.15839Z" fill="white"/> |
| | | </g> |
| | | <g id="Vector 484" opacity="0.3" filter="url(#filter4_f_63_4945)"> |
| | | <path d="M0 0H30.5941V2H2.18529H0V0Z" fill="url(#paint3_linear_63_4945)"/> |
| | | </g> |
| | | <g id="Union" filter="url(#filter5_dd_63_4945)"> |
| | | <path d="M618 16H601.683V14H618V16Z" fill="white"/> |
| | | </g> |
| | | <path id="Union_2" d="M543.555 24H540.495V21H543.555V24Z" fill="#686B8D"/> |
| | | <path id="Union_3" d="M562.931 24H559.871V21H562.931V24Z" fill="#686B8D"/> |
| | | </g> |
| | | <g id="Group 1321318808"> |
| | | <g id="Rectangle 346241141" filter="url(#filter6_f_63_4945)"> |
| | | <path d="M169.287 20H177.446L168.267 30H160.109L169.287 20Z" fill="#0041FF"/> |
| | | </g> |
| | | <g id="Rectangle 346241142" filter="url(#filter7_f_63_4945)"> |
| | | <path d="M183.564 20H191.723L182.545 30H174.386L183.564 20Z" fill="#0041FF" fill-opacity="0.7"/> |
| | | </g> |
| | | <g id="Rectangle 346241143" filter="url(#filter8_f_63_4945)"> |
| | | <path d="M197.842 20H206L196.822 30H188.663L197.842 20Z" fill="#0041FF" fill-opacity="0.3"/> |
| | | </g> |
| | | <g id="Rectangle 346241144" filter="url(#filter9_f_63_4945)"> |
| | | <path d="M212.119 20H220.277L211.099 30H202.941L212.119 20Z" fill="#0041FF" fill-opacity="0.16"/> |
| | | </g> |
| | | </g> |
| | | </g> |
| | | <defs> |
| | | <filter id="filter0_f_63_4945" x="5.57861" y="13.7" width="612.912" height="19.6001" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
| | | <feFlood flood-opacity="0" result="BackgroundImageFix"/> |
| | | <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/> |
| | | <feGaussianBlur stdDeviation="0.5" result="effect1_foregroundBlur_63_4945"/> |
| | | </filter> |
| | | <filter id="filter1_f_63_4945" x="16.3367" y="13.5" width="126.416" height="11" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
| | | <feFlood flood-opacity="0" result="BackgroundImageFix"/> |
| | | <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/> |
| | | <feGaussianBlur stdDeviation="0.5" result="effect1_foregroundBlur_63_4945"/> |
| | | </filter> |
| | | <filter id="filter2_f_63_4945" x="5.1189" y="7" width="4.03955" height="21" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
| | | <feFlood flood-opacity="0" result="BackgroundImageFix"/> |
| | | <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/> |
| | | <feGaussianBlur stdDeviation="0.5" result="effect1_foregroundBlur_63_4945"/> |
| | | </filter> |
| | | <filter id="filter3_ddf_63_4945" x="-0.980225" y="25" width="11.1387" height="11" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
| | | <feFlood flood-opacity="0" result="BackgroundImageFix"/> |
| | | <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="1"/> |
| | | <feComposite in2="hardAlpha" operator="out"/> |
| | | <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.4 0 0 0 0 1 0 0 0 1 0"/> |
| | | <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_63_4945"/> |
| | | <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="1"/> |
| | | <feComposite in2="hardAlpha" operator="out"/> |
| | | <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.4 0 0 0 0 1 0 0 0 1 0"/> |
| | | <feBlend mode="normal" in2="effect1_dropShadow_63_4945" result="effect2_dropShadow_63_4945"/> |
| | | <feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_63_4945" result="shape"/> |
| | | <feGaussianBlur stdDeviation="0.05" result="effect3_foregroundBlur_63_4945"/> |
| | | </filter> |
| | | <filter id="filter4_f_63_4945" x="-0.1" y="-0.1" width="30.794" height="2.2" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
| | | <feFlood flood-opacity="0" result="BackgroundImageFix"/> |
| | | <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/> |
| | | <feGaussianBlur stdDeviation="0.05" result="effect1_foregroundBlur_63_4945"/> |
| | | </filter> |
| | | <filter id="filter5_dd_63_4945" x="595.683" y="8" width="28.3169" height="14" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
| | | <feFlood flood-opacity="0" result="BackgroundImageFix"/> |
| | | <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="3"/> |
| | | <feComposite in2="hardAlpha" operator="out"/> |
| | | <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.4 0 0 0 0 1 0 0 0 1 0"/> |
| | | <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_63_4945"/> |
| | | <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="3"/> |
| | | <feComposite in2="hardAlpha" operator="out"/> |
| | | <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.4 0 0 0 0 1 0 0 0 1 0"/> |
| | | <feBlend mode="normal" in2="effect1_dropShadow_63_4945" result="effect2_dropShadow_63_4945"/> |
| | | <feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_63_4945" result="shape"/> |
| | | </filter> |
| | | <filter id="filter6_f_63_4945" x="159.109" y="19" width="19.3367" height="12" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
| | | <feFlood flood-opacity="0" result="BackgroundImageFix"/> |
| | | <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/> |
| | | <feGaussianBlur stdDeviation="0.5" result="effect1_foregroundBlur_63_4945"/> |
| | | </filter> |
| | | <filter id="filter7_f_63_4945" x="173.386" y="19" width="19.3367" height="12" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
| | | <feFlood flood-opacity="0" result="BackgroundImageFix"/> |
| | | <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/> |
| | | <feGaussianBlur stdDeviation="0.5" result="effect1_foregroundBlur_63_4945"/> |
| | | </filter> |
| | | <filter id="filter8_f_63_4945" x="187.663" y="19" width="19.3367" height="12" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
| | | <feFlood flood-opacity="0" result="BackgroundImageFix"/> |
| | | <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/> |
| | | <feGaussianBlur stdDeviation="0.5" result="effect1_foregroundBlur_63_4945"/> |
| | | </filter> |
| | | <filter id="filter9_f_63_4945" x="201.941" y="19" width="19.3367" height="12" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> |
| | | <feFlood flood-opacity="0" result="BackgroundImageFix"/> |
| | | <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/> |
| | | <feGaussianBlur stdDeviation="0.5" result="effect1_foregroundBlur_63_4945"/> |
| | | </filter> |
| | | <linearGradient id="paint0_linear_63_4945" x1="17.3367" y1="23.5" x2="618" y2="24.02" gradientUnits="userSpaceOnUse"> |
| | | <stop stop-color="#89A1FF"/> |
| | | <stop offset="0.716346" stop-color="#D9DFFF"/> |
| | | <stop offset="1" stop-color="#3C4986" stop-opacity="0"/> |
| | | </linearGradient> |
| | | <linearGradient id="paint1_linear_63_4945" x1="17.3367" y1="18.5" x2="135.634" y2="18.5" gradientUnits="userSpaceOnUse"> |
| | | <stop stop-color="#002AA7"/> |
| | | <stop offset="1" stop-color="#0041FF" stop-opacity="0"/> |
| | | </linearGradient> |
| | | <linearGradient id="paint2_linear_63_4945" x1="7.1387" y1="8" x2="7.1387" y2="27" gradientUnits="userSpaceOnUse"> |
| | | <stop stop-color="#4B598C" stop-opacity="0"/> |
| | | <stop offset="1" stop-color="#8299F2"/> |
| | | </linearGradient> |
| | | <linearGradient id="paint3_linear_63_4945" x1="30.5941" y1="1" x2="0" y2="1" gradientUnits="userSpaceOnUse"> |
| | | <stop stop-color="#999999" stop-opacity="0"/> |
| | | <stop offset="1" stop-color="white"/> |
| | | </linearGradient> |
| | | </defs> |
| | | </svg> |
| New file |
| | |
| | | <template> |
| | | <div class="deviceChart"> |
| | | <div class="titleBg"> |
| | | <span>设备类型分析</span> |
| | | </div> |
| | | <div class="chartBox" ref="chartRef"></div> |
| | | </div> |
| | | </template> |
| | | <script setup> |
| | | import 'echarts-gl' |
| | | import { statisticalDeviceTypeApi } from '@/views/basicManage/deviceStock/fwDevice' |
| | | import useEchartsResize from '@/hooks/useEchartsResize' |
| | | |
| | | const chartRef = ref(null) |
| | | let { chart } = useEchartsResize(chartRef) |
| | | |
| | | const list = ref([]) |
| | | |
| | | const COLORS = ['#2D74EA', '#1BBDCA', '#E3A023', '#FFD900', '#AA4CD3', '#00ADEF'] |
| | | |
| | | const getParametricEquation = (startRatio, endRatio, isSelected, isHovered, k, h) => { |
| | | const midRatio = (startRatio + endRatio) / 2 |
| | | const startRadian = startRatio * Math.PI * 2 |
| | | const endRadian = endRatio * Math.PI * 2 |
| | | const midRadian = midRatio * Math.PI * 2 |
| | | const kValue = typeof k !== 'undefined' ? k : 1 / 3 |
| | | const offsetX = isSelected ? Math.sin(midRadian) * 0.1 : 0 |
| | | const offsetY = isSelected ? Math.cos(midRadian) * 0.1 : 0 |
| | | const hoverRate = isHovered ? 1.05 : 1 |
| | | return { |
| | | u: { |
| | | min: -Math.PI, |
| | | max: Math.PI * 3, |
| | | step: Math.PI / 32, |
| | | }, |
| | | v: { |
| | | min: 0, |
| | | max: Math.PI * 2, |
| | | step: Math.PI / 20, |
| | | }, |
| | | x: (u, v) => { |
| | | if (u < startRadian) return offsetX + Math.cos(startRadian) * (1 + Math.cos(v) * kValue) * hoverRate |
| | | if (u > endRadian) return offsetX + Math.cos(endRadian) * (1 + Math.cos(v) * kValue) * hoverRate |
| | | return offsetX + Math.cos(u) * (1 + Math.cos(v) * kValue) * hoverRate |
| | | }, |
| | | y: (u, v) => { |
| | | if (u < startRadian) return offsetY + Math.sin(startRadian) * (1 + Math.cos(v) * kValue) * hoverRate |
| | | if (u > endRadian) return offsetY + Math.sin(endRadian) * (1 + Math.cos(v) * kValue) * hoverRate |
| | | return offsetY + Math.sin(u) * (1 + Math.cos(v) * kValue) * hoverRate |
| | | }, |
| | | z: (u, v) => { |
| | | if (u < -Math.PI * 0.5) return Math.sin(u) |
| | | if (u > Math.PI * 2.5) return Math.sin(u) * h * 0.1 |
| | | return Math.sin(v) > 0 ? 1 * h * 0.1 : -1 |
| | | }, |
| | | } |
| | | } |
| | | |
| | | const getPie3D = (pieData, internalDiameterRatio) => { |
| | | const series = [] |
| | | let sumValue = 0 |
| | | let startValue = 0 |
| | | let endValue = 0 |
| | | const k = |
| | | typeof internalDiameterRatio !== 'undefined' ? (1 - internalDiameterRatio) / (1 + internalDiameterRatio) : 1 / 3 |
| | | |
| | | for (let i = 0; i < pieData.length; i++) { |
| | | sumValue += pieData[i].value |
| | | const seriesItem = { |
| | | name: typeof pieData[i].name === 'undefined' ? `series${i}` : pieData[i].name, |
| | | type: 'surface', |
| | | parametric: true, |
| | | wireframe: { |
| | | show: false, |
| | | }, |
| | | pieData: pieData[i], |
| | | pieStatus: { |
| | | selected: false, |
| | | hovered: false, |
| | | k: 1 / 10, |
| | | }, |
| | | } |
| | | if (typeof pieData[i].itemStyle !== 'undefined') { |
| | | const itemStyle = {} |
| | | if (typeof pieData[i].itemStyle.color !== 'undefined') itemStyle.color = pieData[i].itemStyle.color |
| | | if (typeof pieData[i].itemStyle.opacity !== 'undefined') itemStyle.opacity = pieData[i].itemStyle.opacity |
| | | seriesItem.itemStyle = itemStyle |
| | | } |
| | | series.push(seriesItem) |
| | | } |
| | | |
| | | for (let i = 0; i < series.length; i++) { |
| | | endValue = startValue + series[i].pieData.value |
| | | series[i].pieData.startRatio = startValue / sumValue |
| | | series[i].pieData.endRatio = endValue / sumValue |
| | | series[i].parametricEquation = getParametricEquation( |
| | | series[i].pieData.startRatio, |
| | | series[i].pieData.endRatio, |
| | | false, |
| | | false, |
| | | k, |
| | | series[i].pieData.value |
| | | ) |
| | | startValue = endValue |
| | | } |
| | | |
| | | return series |
| | | } |
| | | |
| | | const buildOption = () => { |
| | | const legendRich = { |
| | | percent: { |
| | | fontWeight: 700, |
| | | }, |
| | | } |
| | | const optionsData = list.value.map((item, index) => ({ |
| | | name: item.deviceTypeValue, |
| | | value: item.deviceCount, |
| | | itemStyle: { |
| | | color: COLORS[index % COLORS.length], |
| | | }, |
| | | })) |
| | | list.value.forEach((item, index) => { |
| | | legendRich[`count${index}`] = { |
| | | color: COLORS[index % COLORS.length], |
| | | } |
| | | }) |
| | | const series = getPie3D(optionsData, 0.6) |
| | | series.push({ |
| | | name: 'pie2d', |
| | | type: 'pie', |
| | | label: { |
| | | show: false, |
| | | }, |
| | | labelLine: { |
| | | show: false, |
| | | }, |
| | | startAngle: 300, |
| | | clockwise: false, |
| | | radius: ['42%', '54%'], |
| | | center: ['30%', '50%'], |
| | | data: optionsData, |
| | | itemStyle: { |
| | | opacity: 0, |
| | | }, |
| | | }) |
| | | |
| | | return { |
| | | graphic: { |
| | | type: 'text', |
| | | left: '22%', |
| | | top: '35%', |
| | | style: { |
| | | text: '设备类型', |
| | | fill: '#FFFFFF', |
| | | font: '600 14px Arial', |
| | | textAlign: 'center', |
| | | textVerticalAlign: 'middle', |
| | | }, |
| | | }, |
| | | legend: { |
| | | right: 24, |
| | | top: 'center', |
| | | orient: 'vertical', |
| | | textStyle: { |
| | | color: '#FFFFFF', |
| | | fontSize: 12, |
| | | rich: legendRich, |
| | | }, |
| | | itemWidth: 8, |
| | | itemHeight: 8, |
| | | icon: 'rect', |
| | | formatter: name => { |
| | | const itemIndex = list.value.findIndex(v => v.deviceTypeValue === name) |
| | | const item = list.value[itemIndex] |
| | | if (!item) return name |
| | | const percent = item.totalDevices ? Math.round((item.deviceCount / item.totalDevices) * 100) : 0 |
| | | return `${name} {count${itemIndex}|${item.deviceCount}} {percent|${percent}%}` |
| | | }, |
| | | }, |
| | | animation: true, |
| | | tooltip: { |
| | | show: true, |
| | | trigger: 'item', |
| | | confine: true, |
| | | backgroundColor: 'rgba(0,0,0,0.7)', |
| | | borderColor: '#0070FF', |
| | | borderWidth: 1, |
| | | textStyle: { |
| | | color: '#FFFFFF', |
| | | fontSize: 14, // 也可以顺便调整字号 |
| | | }, |
| | | formatter: params => { |
| | | if (params.seriesName !== 'mouseoutSeries' && params.seriesName !== 'pie2d') { |
| | | return `${params.seriesName}:${series[params.seriesIndex].pieData.value}` |
| | | } |
| | | }, |
| | | }, |
| | | title: { |
| | | x: 'center', |
| | | top: '20', |
| | | textStyle: { |
| | | color: '#fff', |
| | | fontSize: 22, |
| | | }, |
| | | }, |
| | | backgroundColor: 'transparent', |
| | | labelLine: { |
| | | show: false, |
| | | }, |
| | | label: { |
| | | show: false, |
| | | }, |
| | | xAxis3D: { |
| | | min: -1, |
| | | max: 1, |
| | | }, |
| | | yAxis3D: { |
| | | min: -1, |
| | | max: 1, |
| | | }, |
| | | zAxis3D: { |
| | | min: -1, |
| | | max: 1, |
| | | }, |
| | | grid3D: { |
| | | show: false, |
| | | boxHeight: 26, |
| | | left: '0%', |
| | | top: -20, |
| | | width: '60%', |
| | | viewControl: { |
| | | distance: 210, |
| | | alpha: 55, |
| | | beta: 70, |
| | | autoRotate: false, |
| | | rotateSensitivity: 1, |
| | | zoomSensitivity: 0, |
| | | panSensitivity: 0, |
| | | }, |
| | | }, |
| | | series, |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | statisticalDeviceTypeApi().then(res => { |
| | | list.value = Array.isArray(res?.data?.data) ? res.data.data : [] |
| | | chart.value.setOption(buildOption()) |
| | | }) |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .deviceChart { |
| | | width: 385px; |
| | | } |
| | | .titleBg { |
| | | width: 365px; |
| | | height: 34px; |
| | | background: url('@/assets/images/basicManage/chartBg.svg') no-repeat center / 100% 100%; |
| | | } |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <div class="deviceChart"> |
| | | <div class="titleBg"> |
| | | <span>设备类型分析</span> |
| | | </div> |
| | | <div class="chartBox" ref="chartRef"></div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import * as echarts from 'echarts' |
| | | import useEchartsResize from '@/hooks/useEchartsResize' |
| | | import { statisticalDeviceOutApi } from '@/views/basicManage/deviceStock/fwDevice' |
| | | |
| | | const chartRef = ref(null) |
| | | let { chart } = useEchartsResize(chartRef) |
| | | |
| | | const list = ref([]) |
| | | function buildOption() { |
| | | const names = list.value.map(item => item.type) |
| | | const values = list.value.map(item => item.count) |
| | | const hasData = values.length > 0 |
| | | const maxValue = values.length ? Math.max(...values) : 0 |
| | | const stepMax = maxValue === 0 ? 10 : Math.ceil(maxValue / 5) * 5 |
| | | const shadowData = values.map(() => stepMax) |
| | | |
| | | return { |
| | | graphic: hasData |
| | | ? [] |
| | | : [ |
| | | { |
| | | type: 'text', |
| | | left: 'center', |
| | | top: 'middle', |
| | | style: { |
| | | text: '暂无数据', |
| | | fill: '#FFFFFF', |
| | | font: '14px Arial', |
| | | textAlign: 'center', |
| | | textVerticalAlign: 'middle', |
| | | }, |
| | | }, |
| | | ], |
| | | grid: { |
| | | top: 15, |
| | | left: 10, |
| | | right: 10, |
| | | bottom: 10, |
| | | containLabel: true, |
| | | }, |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | backgroundColor: 'rgba(0,0,0,0.7)', |
| | | borderColor: '#0070FF', |
| | | borderWidth: 1, |
| | | textStyle: { |
| | | color: '#FFFFFF', |
| | | fontSize: 14, // 也可以顺便调整字号 |
| | | }, |
| | | axisPointer: { |
| | | type: 'shadow', |
| | | }, |
| | | formatter: params => { |
| | | const target = Array.isArray(params) ? params.find(item => item.seriesIndex === 1) : null |
| | | return `${target?.name ?? ''}:${target?.value ?? 0}` |
| | | }, |
| | | }, |
| | | xAxis: { |
| | | type: 'category', |
| | | data: names, |
| | | axisLine: { |
| | | show: false, |
| | | }, |
| | | axisTick: { |
| | | show: false, |
| | | }, |
| | | axisLabel: { |
| | | color: '#FFFFFF', |
| | | fontSize: 12, |
| | | interval: 0, |
| | | }, |
| | | }, |
| | | yAxis: { |
| | | type: 'value', |
| | | axisLabel: { |
| | | color: '#FFFFFF', |
| | | fontSize: 12, |
| | | }, |
| | | axisLine: { |
| | | show: false, |
| | | }, |
| | | axisTick: { |
| | | show: false, |
| | | }, |
| | | splitLine: { |
| | | show: false, |
| | | }, |
| | | }, |
| | | series: [ |
| | | { |
| | | type: 'bar', |
| | | data: shadowData, |
| | | barWidth: 20, |
| | | itemStyle: { |
| | | color: 'rgba(255,255,255,0.08)', |
| | | }, |
| | | z: 0, |
| | | }, |
| | | { |
| | | type: 'bar', |
| | | data: values, |
| | | barWidth: 10, |
| | | barGap: '-75%', |
| | | showBackground: false, |
| | | itemStyle: { |
| | | color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [ |
| | | { offset: 1, color: '#0783FA' }, |
| | | { offset: 0, color: 'rgba(0,85,255,0)' }, |
| | | ]), |
| | | }, |
| | | z: 1, |
| | | }, |
| | | { |
| | | type: 'pictorialBar', |
| | | data: values, |
| | | symbol: 'rect', |
| | | symbolPosition: 'end', |
| | | symbolSize: [10, 3], |
| | | symbolOffset: [2.5, -2], |
| | | itemStyle: { |
| | | color: '#6BB7FF', |
| | | }, |
| | | z: 3, |
| | | }, |
| | | ], |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | statisticalDeviceOutApi().then(res => { |
| | | list.value = Array.isArray(res?.data?.data) ? res.data.data : [] |
| | | chart.value.setOption(buildOption()) |
| | | }) |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .deviceChart { |
| | | width: 638px; |
| | | } |
| | | .titleBg { |
| | | width: 618px; |
| | | height: 34px; |
| | | background: url('@/assets/images/basicManage/chartBg2.svg') no-repeat center / 100% 100%; |
| | | } |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <div class="deviceChart"> |
| | | <div class="titleBg"> |
| | | <span>设备生产厂商统计</span> |
| | | </div> |
| | | <div class="chartBox" ref="chartRef"></div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import * as echarts from 'echarts' |
| | | import useEchartsResize from '@/hooks/useEchartsResize' |
| | | import { statisticalDeviceManufacturerApi, statisticalDeviceOutApi } from '@/views/basicManage/deviceStock/fwDevice' |
| | | |
| | | const chartRef = ref(null) |
| | | let { chart } = useEchartsResize(chartRef) |
| | | |
| | | const list = ref([]) |
| | | function buildOption() { |
| | | const names = list.value.map(item => item.type) |
| | | const values = list.value.map(item => item.count) |
| | | const hasData = values.length > 0 |
| | | |
| | | return { |
| | | graphic: hasData |
| | | ? [] |
| | | : [ |
| | | { |
| | | type: 'text', |
| | | left: 'center', |
| | | top: 'middle', |
| | | style: { |
| | | text: '暂无数据', |
| | | fill: '#FFFFFF', |
| | | font: '14px Arial', |
| | | textAlign: 'center', |
| | | textVerticalAlign: 'middle', |
| | | }, |
| | | }, |
| | | ], |
| | | grid: { |
| | | top: 15, |
| | | left: 10, |
| | | right: 10, |
| | | bottom: 10, |
| | | containLabel: true, |
| | | }, |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | backgroundColor: 'rgba(0,0,0,0.7)', |
| | | borderColor: '#0070FF', |
| | | borderWidth: 1, |
| | | textStyle: { |
| | | color: '#FFFFFF', |
| | | fontSize: 14, // 也可以顺便调整字号 |
| | | }, |
| | | axisPointer: { |
| | | type: 'shadow', |
| | | }, |
| | | formatter: params => { |
| | | const target = Array.isArray(params) ? params.find(item => item.seriesIndex === 1) : null |
| | | return `${target?.name ?? ''}:${target?.value ?? 0}` |
| | | }, |
| | | }, |
| | | |
| | | xAxis: { |
| | | type: 'category', |
| | | data: names, |
| | | axisLine: { |
| | | show: false, |
| | | }, |
| | | axisTick: { |
| | | show: false, |
| | | }, |
| | | axisLabel: { |
| | | color: '#FFFFFF', |
| | | fontSize: 12, |
| | | interval: 0, |
| | | }, |
| | | }, |
| | | yAxis: { |
| | | type: 'value', |
| | | axisLabel: { |
| | | color: '#FFFFFF', |
| | | fontSize: 12, |
| | | }, |
| | | axisLine: { |
| | | show: false, |
| | | }, |
| | | axisTick: { |
| | | show: false, |
| | | }, |
| | | splitLine: { |
| | | show: false, |
| | | }, |
| | | }, |
| | | series: [ |
| | | { |
| | | type: 'bar', |
| | | data: values, |
| | | barWidth: 10, |
| | | itemStyle: { |
| | | color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [ |
| | | { offset: 1, color: '#0783FA' }, |
| | | { offset: 0, color: 'rgba(0,85,255,0)' }, |
| | | ]), |
| | | }, |
| | | z: 1, |
| | | }, |
| | | { |
| | | type: 'pictorialBar', |
| | | itemStyle: { |
| | | color: '#0B0B17', |
| | | }, |
| | | symbolRepeat: 'fixed', |
| | | symbolMargin: 3, |
| | | symbol: 'rect', |
| | | symbolClip: false, |
| | | symbolPosition: 'end', |
| | | symbolSize: [10, 3], |
| | | data: values, |
| | | z: 2, |
| | | }, |
| | | ], |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | statisticalDeviceManufacturerApi().then(res => { |
| | | list.value = Array.isArray(res?.data?.data) ? res.data.data : [] |
| | | chart.value.setOption(buildOption()) |
| | | }) |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .deviceChart { |
| | | width: 638px; |
| | | } |
| | | .titleBg { |
| | | width: 618px; |
| | | height: 34px; |
| | | background: url('@/assets/images/basicManage/chartBg2.svg') no-repeat center / 100% 100%; |
| | | } |
| | | </style> |
| | |
| | | responseType: 'blob', |
| | | }) |
| | | } |
| | | |
| | | //设备类型统计 |
| | | export const statisticalDeviceTypeApi = params => { |
| | | return request({ |
| | | url: `/drone-fw/device/fwDevice/statisticalDeviceType`, |
| | | method: 'get', |
| | | params, |
| | | }) |
| | | } |
| | | //设备出库去向统计 |
| | | export const statisticalDeviceOutApi = params => { |
| | | return request({ |
| | | url: `/drone-fw/device/fwDevice/statisticalDeviceOut`, |
| | | method: 'get', |
| | | params, |
| | | }) |
| | | } |
| | | //设备生产厂商统计 |
| | | export const statisticalDeviceManufacturerApi = params => { |
| | | return request({ |
| | | url: `/drone-fw/device/fwDevice/statisticalDeviceManufacturer`, |
| | | method: 'get', |
| | | params, |
| | | }) |
| | | } |
| | |
| | | <template> |
| | | <basic-container> |
| | | <div class="chart"> |
| | | <DeviceChart1 /> |
| | | <DeviceChart2 /> |
| | | <DeviceChart3 /> |
| | | </div> |
| | | <el-form ref="queryParamsRef" :model="searchParams" class="ztzf-page-history-search"> |
| | | <el-form-item label="名称" prop="deviceName"> |
| | | <el-input |
| | |
| | | import { blobDownload, dateRangeFormat, getDictLabel, formatDateToSlash } from '@ztzf/utils' |
| | | import DeviceTrackDiaLog from '@/views/basicManage/deviceStock/DeviceTrackDiaLog.vue' |
| | | import DeviceScrapDiaLog from '@/views/basicManage/deviceStock/DeviceScrapDiaLog.vue' |
| | | import DeviceChart1 from '@/views/basicManage/deviceStock/DeviceChart1.vue' |
| | | import DeviceChart2 from '@/views/basicManage/deviceStock/DeviceChart2.vue' |
| | | import DeviceChart3 from '@/views/basicManage/deviceStock/DeviceChart3.vue' |
| | | |
| | | const initSearchParams = () => ({ |
| | | deviceName: '', // 设备名称 |
| | |
| | | getDeptTreeFun() |
| | | }) |
| | | </script> |
| | | <style scoped lang="scss"></style> |
| | | <style scoped lang="scss"> |
| | | .chart { |
| | | width: 100%; |
| | | height: 234px; |
| | | display: flex; |
| | | :deep() { |
| | | * { |
| | | box-sizing: border-box; |
| | | } |
| | | .deviceChart { |
| | | |
| | | height: 100%; |
| | | background: rgba(26, 26, 42, 0.3); |
| | | border-radius: 10px 10px 10px 10px; |
| | | position: relative; |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 10px; |
| | | |
| | | .titleBg { |
| | | font-weight: 400; |
| | | font-size: 18px; |
| | | color: #ffffff; |
| | | font-family: PangMen, serif; |
| | | padding-left: 22px; |
| | | } |
| | | .chartBox { |
| | | width: 100%; |
| | | height: 0; |
| | | flex-grow: 1; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | </style> |
| | |
| | | <template> |
| | | <div class="call-container" :class="{'incoming-call': state === 'ringing'} "> |
| | | <div class="content-wrapper"> |
| | | <!-- <div class="input-group">--> |
| | | <!-- <div class="input-item">--> |
| | | <!-- <span class="input-label">我的 userId:</span>--> |
| | | <!-- <input v-model="uid" class="input-field" />--> |
| | | <!-- </div>--> |
| | | <!-- --> |
| | | <!-- <div class="input-item">--> |
| | | <!-- <span class="input-label">对方 userId:</span>--> |
| | | <!-- <input v-model="peerUid" class="input-field" />--> |
| | | <!-- </div>--> |
| | | <!-- </div>--> |
| | | |
| | | <div class="button-group" v-if="false"> |
| | | <button @click="connectWS" :disabled="connected" class="btn btn-connect">连接WS</button> |
| | | </div> |
| | | <!-- 头像--> |
| | | <div class="contactAvatar"> |
| | | <img :src="defaultAvatar" alt=""></img> |
| | | </div> |
| | | <div class="status-group"> |
| | | <strong v-if="state==='ringing'" class="status-text incoming-status">来电中...</strong> |
| | | <strong v-if="state==='calling'" class="status-text calling-status">呼叫中...</strong> |
| | | <!-- <strong class="status-text">状态:<span :style="{color: getStateColor(state)}">{{ state }}</span></strong>--> |
| | | <strong v-if="state==='in-call'" class="status-text">通话时长:{{ durationText }}</strong> |
| | | </div> |
| | | <div class="button-group"> |
| | | <button v-if="false" @click="requestCall" :disabled="!connected || state!=='idle'" class="btn btn-call">呼叫</button> |
| | | |
| | | <button v-if="state==='ringing'" @click="acceptCall" class="btn btn-accept incoming-accept">接听</button> |
| | | <button v-if="state==='ringing'" @click="rejectCall" class="btn btn-reject incoming-reject">拒绝</button> |
| | | |
| | | <button v-if="state==='calling' || state==='in-call'" @click="hangup" class="btn btn-hangup">挂断</button> |
| | | </div> |
| | | |
| | | <div class="content-wrapper"> |
| | | <!-- <div class="input-group"> |
| | | <div class="input-item"> |
| | | <span class="input-label">我的 userId:</span> |
| | | <input v-model="uid" class="input-field" /> |
| | | </div> |
| | | |
| | | </div> |
| | | |
| | | <!-- <div class="tip-message">--> |
| | | <!-- <small>提示:若 getUserMedia 不可用,请用 https 或 localhost 访问页面。</small>--> |
| | | <!-- </div>--> |
| | | |
| | | <!-- <div class="log-container">--> |
| | | <!-- <div class="log-box">--> |
| | | <!-- <div class="log-title">日志信息:</div>--> |
| | | <!-- <pre class="log-content">{{ logText }}</pre>--> |
| | | <!-- </div>--> |
| | | <!-- </div>--> |
| | | |
| | | <audio ref="remoteAudio" autoplay class="hidden-audio"></audio> |
| | | <div class="input-item"> |
| | | <span class="input-label">对方 userId:</span> |
| | | <input v-model="peerUid" class="input-field" /> |
| | | </div> |
| | | </div>--> |
| | | |
| | | <div class="button-group" v-if="false"> |
| | | <button @click="connectWS" :disabled="connected" class="btn btn-connect">连接WS</button> |
| | | </div> |
| | | <!-- 头像--> |
| | | <div class="contactAvatar"> |
| | | <img :src="defaultAvatar" alt=""></img> |
| | | </div> |
| | | <div class="status-group"> |
| | | <strong v-if="state==='ringing'" class="status-text incoming-status">来电中...</strong> |
| | | <strong v-if="state==='calling'" class="status-text calling-status">呼叫中...</strong> |
| | | <!-- <strong class="status-text">状态:<span :style="{color: getStateColor(state)}">{{ state }}</span></strong>--> |
| | | <strong v-if="state==='in-call'" class="status-text">通话时长:{{ durationText }}</strong> |
| | | </div> |
| | | <div class="button-group"> |
| | | <button v-if="state === 'idle' && !receiveParameters" @click="requestCall" :disabled="!connected" class="btn btn-call">呼叫</button> |
| | | |
| | | <button v-if="state==='ringing'" @click="acceptCall" class="btn btn-accept incoming-accept">接听</button> |
| | | <button v-if="state==='ringing'" @click="rejectCall" class="btn btn-reject incoming-reject">拒绝</button> |
| | | |
| | | <button v-if="state==='calling' || state==='in-call'" @click="hangup" class="btn btn-hangup">挂断</button> |
| | | </div> |
| | | |
| | | |
| | | </div> |
| | | |
| | | <!-- <div class="tip-message"> |
| | | <small>提示:若 getUserMedia 不可用,请用 https 或 localhost 访问页面。</small> |
| | | </div>--> |
| | | |
| | | <!-- <div class="log-container"> |
| | | <div class="log-box"> |
| | | <div class="log-title">日志信息:</div> |
| | | <pre class="log-content">{{ logText }}</pre> |
| | | </div> |
| | | </div>--> |
| | | |
| | | <audio ref="remoteAudio" autoplay class="hidden-audio"></audio> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | /** ✅ 你的 WS 地址前缀(后面拼 userId) */ |
| | | const WS_BASE = 'wss://wrj.shuixiongit.com/ws/chat?userId=' |
| | | |
| | | // 解析URL参数 |
| | | const parseUrlParams = () => { |
| | | const url = new URL(window.location.href) |
| | | const paramsStr = url.searchParams.get('params') |
| | | if (paramsStr) { |
| | | try { |
| | | const params = JSON.parse(decodeURIComponent(paramsStr)) |
| | | if (params.peerUid) { |
| | | peerUid.value = String(params.peerUid) |
| | | log('🔗 从URL参数获取peerUid:', params.peerUid) |
| | | } |
| | | } catch (e) { |
| | | log('❌ 解析URL参数失败:', String(e)) |
| | | } |
| | | } |
| | | } |
| | | // 解析URL参数 |
| | | const parseUrlParams = () => { |
| | | // 1. 优先从route.query.voiceparams获取参数 |
| | | if (route.query && route.query.voiceparams) { |
| | | console.log('从route.query.voiceparams获取参数:', route.query.voiceparams); |
| | | try { |
| | | // 解码并解析voiceparams参数 |
| | | const params = JSON.parse(decodeURIComponent(route.query.voiceparams)); |
| | | console.log('解析后的voiceparams:', params); |
| | | |
| | | if (params.peerUid) { |
| | | peerUid.value = String(params.peerUid); |
| | | log('🔗 从voiceparams获取peerUid:', params.peerUid); |
| | | } |
| | | |
| | | // 如果是来电类型,直接触发来电状态 |
| | | if (params.type === 'incoming') { |
| | | log('📞 voiceparams检测到来电,直接进入来电状态'); |
| | | incomingFrom = String(params.from); |
| | | peerUid.value = incomingFrom; |
| | | acceptedByMe = false; |
| | | offeredByPeer = false; |
| | | |
| | | state.value = 'ringing'; |
| | | log('📞 收到来电 call,来自', incomingFrom); |
| | | |
| | | // 开始来电提示(铃声和震动) |
| | | playRingtone(); |
| | | startVibration(); |
| | | } |
| | | } catch (e) { |
| | | log('❌ 解析voiceparams参数失败:', String(e)); |
| | | } |
| | | } |
| | | |
| | | // 2. 同时保留原有的URL参数解析逻辑,作为备选 |
| | | const url = new URL(window.location.href); |
| | | const paramsStr = url.searchParams.get('params'); |
| | | if (paramsStr) { |
| | | try { |
| | | const params = JSON.parse(decodeURIComponent(paramsStr)); |
| | | if (params.peerUid) { |
| | | peerUid.value = String(params.peerUid); |
| | | log('🔗 从URL参数获取peerUid:', params.peerUid); |
| | | } |
| | | } catch (e) { |
| | | log('❌ 解析URL参数失败:', String(e)); |
| | | } |
| | | } |
| | | } |
| | | |
| | | const uid = ref('3') |
| | | const peerUid = ref('2') |
| | | // 从URL参数或本地存储获取用户ID |
| | | const getUserIdFromStorage = () => { |
| | | const userId = localStorage.getItem('userId') || sessionStorage.getItem('userId') |
| | | if (userId) { |
| | | log('🔗 从本地存储获取userId:', userId) |
| | | return userId |
| | | } |
| | | return '3' // 默认值 |
| | | } |
| | | |
| | | const uid = ref('3') |
| | | const peerUid = ref('2') |
| | | |
| | | const connected = ref(false) |
| | | const state = ref('idle') // idle | calling | ringing | in-call |
| | |
| | | const m = String(Math.floor(seconds.value / 60)).padStart(2, '0') |
| | | const s = String(seconds.value % 60).padStart(2, '0') |
| | | return `${m}:${s}` |
| | | }) |
| | | function startTimer() { |
| | | }) |
| | | function startTimer() { |
| | | stopTimer() |
| | | seconds.value = 0 |
| | | timer = setInterval(() => seconds.value++, 1000) |
| | | } |
| | | function stopTimer() { |
| | | } |
| | | function stopTimer() { |
| | | if (timer) clearInterval(timer) |
| | | timer = null |
| | | } |
| | | |
| | | // 心跳 |
| | | let pingTimer = null |
| | | function startPing() { |
| | | } |
| | | |
| | | // 心跳 |
| | | let pingTimer = null |
| | | function startPing() { |
| | | stopPing() |
| | | // 每 25 秒发一次 ping(你后端有 PING/PONG) |
| | | pingTimer = setInterval(() => { |
| | | if (connected.value) send('ping', 'system', null) |
| | | if (connected.value) send('ping', 'system', null) |
| | | }, 25000) |
| | | } |
| | | } |
| | | |
| | | // 来电提示相关 |
| | | let ringtoneAudio = null |
| | | let vibrationTimer = null |
| | | // 来电提示相关 |
| | | let ringtoneAudio = null |
| | | let vibrationTimer = null |
| | | |
| | | // 播放来电铃声 |
| | | function playRingtone() { |
| | | // 播放来电铃声 |
| | | function playRingtone() { |
| | | // 创建音频对象(使用默认的系统提示音) |
| | | ringtoneAudio = new Audio('data:audio/wav;base64,UklGRigAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQAAAAAAA=='); |
| | | ringtoneAudio.loop = true; |
| | | ringtoneAudio.volume = 0.5; |
| | | |
| | | |
| | | try { |
| | | ringtoneAudio.play(); |
| | | log('🔊 开始播放来电铃声') |
| | | ringtoneAudio.play(); |
| | | log('🔊 开始播放来电铃声') |
| | | } catch (e) { |
| | | log('❌ 播放来电铃声失败:', String(e)) |
| | | log('❌ 播放来电铃声失败:', String(e)) |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 停止来电铃声 |
| | | function stopRingtone() { |
| | | // 停止来电铃声 |
| | | function stopRingtone() { |
| | | if (ringtoneAudio) { |
| | | try { |
| | | ringtoneAudio.pause(); |
| | | ringtoneAudio.currentTime = 0; |
| | | ringtoneAudio = null; |
| | | log('🔇 停止播放来电铃声') |
| | | } catch (e) { |
| | | log('❌ 停止来电铃声失败:', String(e)) |
| | | } |
| | | try { |
| | | ringtoneAudio.pause(); |
| | | ringtoneAudio.currentTime = 0; |
| | | ringtoneAudio = null; |
| | | log('🔇 停止播放来电铃声') |
| | | } catch (e) { |
| | | log('❌ 停止来电铃声失败:', String(e)) |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 开始震动 |
| | | function startVibration() { |
| | | // 开始震动 |
| | | function startVibration() { |
| | | // 检查浏览器是否支持震动 API |
| | | if ('vibrate' in navigator) { |
| | | // 震动模式:震动1秒,暂停0.5秒,重复 |
| | | const vibrationPattern = [1000, 500]; |
| | | |
| | | try { |
| | | navigator.vibrate(vibrationPattern); |
| | | vibrationTimer = setInterval(() => { |
| | | navigator.vibrate(vibrationPattern); |
| | | }, 1500); // 每1.5秒重复一次震动模式 |
| | | log('📳 开始震动提示') |
| | | } catch (e) { |
| | | log('❌ 震动失败:', String(e)) |
| | | } |
| | | } else { |
| | | log('ℹ️ 当前浏览器不支持震动功能') |
| | | } |
| | | } |
| | | // 震动模式:震动1秒,暂停0.5秒,重复 |
| | | const vibrationPattern = [1000, 500]; |
| | | |
| | | // 停止震动 |
| | | function stopVibration() { |
| | | try { |
| | | navigator.vibrate(vibrationPattern); |
| | | vibrationTimer = setInterval(() => { |
| | | navigator.vibrate(vibrationPattern); |
| | | }, 1500); // 每1.5秒重复一次震动模式 |
| | | log('📳 开始震动提示') |
| | | } catch (e) { |
| | | log('❌ 震动失败:', String(e)) |
| | | } |
| | | } else { |
| | | log('ℹ️ 当前浏览器不支持震动功能') |
| | | } |
| | | } |
| | | |
| | | // 停止震动 |
| | | function stopVibration() { |
| | | if (vibrationTimer) { |
| | | clearInterval(vibrationTimer); |
| | | vibrationTimer = null; |
| | | clearInterval(vibrationTimer); |
| | | vibrationTimer = null; |
| | | } |
| | | |
| | | |
| | | if ('vibrate' in navigator) { |
| | | try { |
| | | navigator.vibrate(0); // 停止震动 |
| | | log('📳 停止震动提示') |
| | | } catch (e) { |
| | | log('❌ 停止震动失败:', String(e)) |
| | | } |
| | | try { |
| | | navigator.vibrate(0); // 停止震动 |
| | | log('📳 停止震动提示') |
| | | } catch (e) { |
| | | log('❌ 停止震动失败:', String(e)) |
| | | } |
| | | } |
| | | } |
| | | function stopPing() { |
| | | } |
| | | function stopPing() { |
| | | if (pingTimer) clearInterval(pingTimer) |
| | | pingTimer = null |
| | | } |
| | | |
| | | function log(...args) { |
| | | } |
| | | |
| | | function log(...args) { |
| | | const line = args.map(v => (typeof v === 'string' ? v : JSON.stringify(v))).join(' ') |
| | | logText.value += `[${new Date().toLocaleTimeString()}] ${line}\n` |
| | | console.log(...args) |
| | | } |
| | | |
| | | /** |
| | | * ✅ 严格按后端 ChatMessage 字段发: |
| | | * { type, from, to, payload, timestamp } |
| | | * 注意:后端会用 session 的 userId 覆盖 from,但我们带上也没坏处 |
| | | */ |
| | | function send(type, to, payload) { |
| | | } |
| | | |
| | | /** |
| | | * ✅ 严格按后端 ChatMessage 字段发: |
| | | * { type, from, to, payload, timestamp } |
| | | * 注意:后端会用 session 的 userId 覆盖 from,但我们带上也没坏处 |
| | | */ |
| | | function send(type, to, payload) { |
| | | if (!type) return |
| | | if (!ws || ws.readyState !== WebSocket.OPEN) return |
| | | |
| | | |
| | | const msg = { |
| | | type, // "call/accept/busy/offer/answer/candidate/hangup/ping" |
| | | from: String(uid.value), |
| | | to: String(to), |
| | | payload: payload ?? null, |
| | | timestamp: Date.now(), |
| | | type, // "call/accept/busy/offer/answer/candidate/hangup/ping" |
| | | from: String(uid.value), |
| | | to: String(to), |
| | | payload: payload ?? null, |
| | | timestamp: Date.now(), |
| | | } |
| | | ws.send(JSON.stringify(msg)) |
| | | } |
| | | |
| | | /* ---------------- WebRTC ---------------- */ |
| | | |
| | | function createPC() { |
| | | } |
| | | |
| | | /* ---------------- WebRTC ---------------- */ |
| | | |
| | | function createPC() { |
| | | pc = new RTCPeerConnection({ |
| | | iceServers: [{ urls: 'stun:stun.l.google.com:19302' }], |
| | | }) |
| | |
| | | /* ---------------- 信令处理 ---------------- */ |
| | | |
| | | async function onSignal(msg) { |
| | | console.log('1cesss',msg); |
| | | |
| | | if (!msg) return |
| | | const t = (msg.type || '').toString() |
| | | |
| | |
| | | |
| | | state.value = 'ringing' |
| | | log('📞 收到来电 call,来自', incomingFrom) |
| | | |
| | | |
| | | // 开始来电提示(铃声和震动) |
| | | playRingtone() |
| | | startVibration() |
| | | |
| | | |
| | | return |
| | | } |
| | | |
| | |
| | | cleanup(false) |
| | | return |
| | | } |
| | | |
| | | |
| | | if (t === 'offer') { |
| | | // 被叫收到 offer:只有点击接听后才真正应答(你要求“先点接听”) |
| | | offeredByPeer = true |
| | | incomingFrom = String(msg.from) |
| | | peerUid.value = incomingFrom |
| | | |
| | | if (!acceptedByMe) { |
| | | state.value = 'ringing' |
| | | log('📩 收到 offer(但未接听),已等待用户点击接听') |
| | | // 暂存 offer 到一个变量 |
| | | lastOfferSdp = msg.payload?.sdp |
| | | // 被叫收到 offer:只有点击接听后才真正应答(你要求“先点接听”) |
| | | offeredByPeer = true |
| | | incomingFrom = String(msg.from) |
| | | peerUid.value = incomingFrom |
| | | |
| | | if (!acceptedByMe) { |
| | | state.value = 'ringing' |
| | | log('📩 收到 offer(但未接听),已等待用户点击接听') |
| | | // 暂存 offer 到一个变量 |
| | | lastOfferSdp = msg.payload?.sdp |
| | | return |
| | | } |
| | | |
| | | // 已接听:直接 answer |
| | | await answerOffer(msg.payload?.sdp, incomingFrom) |
| | | return |
| | | } |
| | | |
| | | // 已接听:直接 answer |
| | | await answerOffer(msg.payload?.sdp, incomingFrom) |
| | | return |
| | | } |
| | | |
| | | |
| | | if (t === 'answer') { |
| | | log('✅ 收到 answer,通话建立') |
| | | if (!pc) createPC() |
| | | await pc.setRemoteDescription(new RTCSessionDescription(msg.payload?.sdp)) |
| | | await flushCandidates() |
| | | |
| | | state.value = 'in-call' |
| | | startTimer() |
| | | return |
| | | log('✅ 收到 answer,通话建立') |
| | | if (!pc) createPC() |
| | | await pc.setRemoteDescription(new RTCSessionDescription(msg.payload?.sdp)) |
| | | await flushCandidates() |
| | | |
| | | state.value = 'in-call' |
| | | startTimer() |
| | | return |
| | | } |
| | | |
| | | |
| | | if (t === 'candidate') { |
| | | const c = msg.payload?.candidate |
| | | if (!c) return |
| | | if (pc && pc.remoteDescription) { |
| | | await pc.addIceCandidate(new RTCIceCandidate(c)) |
| | | } else { |
| | | pendingCandidates.push(c) |
| | | } |
| | | return |
| | | const c = msg.payload?.candidate |
| | | if (!c) return |
| | | if (pc && pc.remoteDescription) { |
| | | await pc.addIceCandidate(new RTCIceCandidate(c)) |
| | | } else { |
| | | pendingCandidates.push(c) |
| | | } |
| | | return |
| | | } |
| | | |
| | | |
| | | if (t === 'hangup') { |
| | | log('📴 对方挂断') |
| | | cleanup(false) |
| | | return |
| | | log('📴 对方挂断') |
| | | cleanup(false) |
| | | return |
| | | } |
| | | |
| | | |
| | | if (t === 'pong') { |
| | | // 心跳响应 |
| | | // log('pong') |
| | | return |
| | | // 心跳响应 |
| | | // log('pong') |
| | | return |
| | | } |
| | | } |
| | | |
| | | // 暂存 offer(用于“先点接听再 answer”) |
| | | let lastOfferSdp = null |
| | | |
| | | /* ---------------- 业务操作 ---------------- */ |
| | | |
| | | function connectWS() { |
| | | } |
| | | |
| | | // 暂存 offer(用于“先点接听再 answer”) |
| | | let lastOfferSdp = null |
| | | |
| | | /* ---------------- 业务操作 ---------------- */ |
| | | |
| | | function connectWS() { |
| | | // 确保获取最新的用户ID |
| | | uid.value = getUserIdFromStorage() |
| | | |
| | | if (ws) ws.close() |
| | | |
| | | |
| | | ws = new WebSocket(WS_BASE + encodeURIComponent(uid.value)) |
| | | |
| | | |
| | | ws.onopen = () => { |
| | | connected.value = true |
| | | log('WS 已连接 userId=', uid.value) |
| | | startPing() |
| | | connected.value = true |
| | | log('WS 已连接 userId=', uid.value) |
| | | startPing() |
| | | } |
| | | |
| | | |
| | | ws.onclose = () => { |
| | | connected.value = false |
| | | log('WS 已关闭') |
| | | stopPing() |
| | | connected.value = false |
| | | log('WS 已关闭') |
| | | stopPing() |
| | | } |
| | | |
| | | |
| | | ws.onerror = (e) => log('WS error', e) |
| | | |
| | | |
| | | ws.onmessage = (e) => { |
| | | const msg = JSON.parse(e.data) |
| | | log('收到信令', msg) |
| | | onSignal(msg) |
| | | const msg = JSON.parse(e.data) |
| | | log('收到信令', msg) |
| | | onSignal(msg) |
| | | } |
| | | } |
| | | |
| | | function requestCall() { |
| | | } |
| | | |
| | | function requestCall() { |
| | | state.value = 'calling' |
| | | incomingFrom = null |
| | | acceptedByMe = false |
| | | offeredByPeer = false |
| | | lastOfferSdp = null |
| | | |
| | | |
| | | send('call', peerUid.value, null) |
| | | log('➡️ 发起呼叫 call 给', peerUid.value) |
| | | } |
| | | |
| | | function acceptCall() { |
| | | } |
| | | |
| | | function acceptCall() { |
| | | if (!incomingFrom) return |
| | | acceptedByMe = true |
| | | |
| | |
| | | |
| | | // 如果 offer 已经提前到达(我们暂存了),立刻 answer |
| | | if (lastOfferSdp) { |
| | | answerOffer(lastOfferSdp, incomingFrom) |
| | | lastOfferSdp = null |
| | | answerOffer(lastOfferSdp, incomingFrom) |
| | | lastOfferSdp = null |
| | | } |
| | | } |
| | | |
| | | function rejectCall() { |
| | | } |
| | | |
| | | function rejectCall() { |
| | | if (!incomingFrom) return |
| | | |
| | | // 停止来电提示 |
| | |
| | | // 你后端没有 reject,用 busy 表示拒绝/不可接听 |
| | | send('busy', incomingFrom, null) |
| | | log('❌ 已拒绝来电,发送 busy 给', incomingFrom) |
| | | |
| | | |
| | | incomingFrom = null |
| | | acceptedByMe = false |
| | | offeredByPeer = false |
| | | lastOfferSdp = null |
| | | state.value = 'idle' |
| | | |
| | | |
| | | // 返回上一页(uni-app的语音通话列表页面) |
| | | if (window.uni) { |
| | | window.uni.navigateBack() |
| | | window.uni.navigateBack() |
| | | } else { |
| | | log('❌ uni对象不存在,无法返回上一页') |
| | | log('❌ uni对象不存在,无法返回上一页') |
| | | } |
| | | } |
| | | |
| | | async function startOffer() { |
| | | } |
| | | |
| | | async function startOffer() { |
| | | if (!pc) createPC() |
| | | |
| | | |
| | | // ✅ 不会重复 addTrack |
| | | await ensureLocalTracksAdded() |
| | | |
| | | |
| | | const offer = await pc.createOffer() |
| | | await pc.setLocalDescription(offer) |
| | | |
| | | |
| | | send('offer', peerUid.value, { sdp: pc.localDescription }) |
| | | log('➡️ 已发送 offer 给', peerUid.value) |
| | | } |
| | | |
| | | async function answerOffer(offerSdp, from) { |
| | | } |
| | | |
| | | async function answerOffer(offerSdp, from) { |
| | | if (!offerSdp) { |
| | | log('❌ offer sdp 为空,无法接听') |
| | | return |
| | | log('❌ offer sdp 为空,无法接听') |
| | | return |
| | | } |
| | | if (!pc) createPC() |
| | | |
| | | |
| | | await pc.setRemoteDescription(new RTCSessionDescription(offerSdp)) |
| | | await flushCandidates() |
| | | |
| | | |
| | | await ensureLocalTracksAdded() |
| | | |
| | | |
| | | const answer = await pc.createAnswer() |
| | | await pc.setLocalDescription(answer) |
| | | |
| | | |
| | | send('answer', from, { sdp: pc.localDescription }) |
| | | log('⬅️ 已发送 answer 给', from) |
| | | |
| | | |
| | | state.value = 'in-call' |
| | | startTimer() |
| | | } |
| | | |
| | | function hangup() { |
| | | } |
| | | |
| | | function hangup() { |
| | | cleanup(true) |
| | | } |
| | | |
| | | /* ---------------- 清理 ---------------- */ |
| | | |
| | | function cleanup(sendToPeer) { |
| | | } |
| | | |
| | | /* ---------------- 清理 ---------------- */ |
| | | |
| | | function cleanup(sendToPeer) { |
| | | try { |
| | | if (sendToPeer) send('hangup', peerUid.value, null) |
| | | if (sendToPeer) send('hangup', peerUid.value, null) |
| | | } catch {} |
| | | |
| | | |
| | | stopTimer() |
| | | // 停止来电提示 |
| | | stopRingtone() |
| | | stopVibration() |
| | | |
| | | if (pc) { |
| | | pc.getSenders().forEach(s => s.track && s.track.stop()) |
| | | pc.close() |
| | | pc.getSenders().forEach(s => s.track && s.track.stop()) |
| | | pc.close() |
| | | } |
| | | if (localStream) { |
| | | localStream.getTracks().forEach(t => t.stop()) |
| | | localStream.getTracks().forEach(t => t.stop()) |
| | | } |
| | | |
| | | |
| | | pc = null |
| | | localStream = null |
| | | pendingCandidates.length = 0 |
| | | |
| | | |
| | | incomingFrom = null |
| | | acceptedByMe = false |
| | | offeredByPeer = false |
| | | lastOfferSdp = null |
| | | |
| | | |
| | | state.value = 'idle' |
| | | |
| | | |
| | | // 返回上一页(uni-app的语音通话列表页面) |
| | | if (window.uni) { |
| | | window.uni.navigateBack() |
| | | window.uni.navigateBack() |
| | | } else { |
| | | log('❌ uni对象不存在,无法返回上一页') |
| | | log('❌ uni对象不存在,无法返回上一页') |
| | | } |
| | | } |
| | | |
| | | // 定义receiveParameters变量,用于模板中判断是否显示呼叫按钮 |
| | | const receiveParameters = ref(route.query.contact || null); |
| | | |
| | | onMounted(() => { |
| | | const receiveParameters = route.query.contact |
| | | console.log('接收参数', receiveParameters) |
| | | console.log('收拾收拾',route.query.voiceparams); |
| | | console.log('接收参数', receiveParameters.value) |
| | | |
| | | // 解析并使用contact参数 |
| | | if (receiveParameters.value) { |
| | | try { |
| | | const contact = JSON.parse(decodeURIComponent(receiveParameters.value)) |
| | | if (contact.friendId) { |
| | | peerUid.value = contact.friendId |
| | | log('🔗 从contact参数获取peerUid:', contact.friendId) |
| | | log('👤 联系人名称:', contact.friendNickName || '未知联系人') |
| | | } |
| | | } catch (e) { |
| | | log('❌ 解析contact参数失败:', String(e)) |
| | | } |
| | | } |
| | | |
| | | // 解析URL参数 |
| | | parseUrlParams() |
| | | // 自动连接WebSocket |
| | | connectWS() |
| | | }) |
| | | }); |
| | | |
| | | // 监听连接状态,当connected变为true且有receiveParameters时自动呼叫 |
| | | watch(() => connected.value, (newVal) => { |
| | | if (newVal && state.value === 'idle' && receiveParameters.value) { |
| | | log('📞 连接成功且有contact参数,自动发起呼叫'); |
| | | requestCall(); |
| | | } |
| | | }); |
| | | |
| | | // 监听连接状态,连接成功后自动呼叫 |
| | | watch(() => connected.value, (newVal) => { |
| | | if (newVal && state.value === 'idle') { |
| | | requestCall() |
| | | } |
| | | // watch(() => connected.value, (newVal) => { |
| | | // if (newVal && state.value === 'idle') { |
| | | // requestCall() |
| | | // } |
| | | // }) |
| | | |
| | | onBeforeUnmount(() => { |
| | | cleanup(false) |
| | | stopPing() |
| | | if (ws) ws.close() |
| | | }) |
| | | </script> |
| | | <style scoped lang="scss"> |
| | | .call-container { |
| | | padding: 20px; |
| | | min-height: 100vh; |
| | | font-family: Arial, sans-serif; |
| | | background-color: #f5f5f5; |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | onBeforeUnmount(() => { |
| | | cleanup(false) |
| | | stopPing() |
| | | if (ws) ws.close() |
| | | }) |
| | | </script> |
| | | <style scoped lang="scss"> |
| | | .call-container { |
| | | padding: 20px; |
| | | min-height: 100vh; |
| | | font-family: Arial, sans-serif; |
| | | background-color: #f5f5f5; |
| | | transition: all 0.3s ease; |
| | | /* 来电时的全屏样式 */ |
| | | .call-container.incoming-call { |
| | | position: fixed; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | z-index: 99999; |
| | | background-color: #ffffff; |
| | | padding: 0; |
| | | } |
| | | |
| | | .content-wrapper { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 20px; |
| | | max-width: 600px; |
| | | margin: 0 auto; |
| | | height: 100vh; |
| | | justify-content: center; |
| | | align-items: center; |
| | | } |
| | | |
| | | .input-group { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .input-item { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | .input-label { |
| | | font-weight: bold; |
| | | color: #333; |
| | | } |
| | | |
| | | .input-field { |
| | | width: 120px; |
| | | padding: 8px; |
| | | border: 1px solid #ddd; |
| | | border-radius: 4px; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .button-group { |
| | | display: flex; |
| | | gap: 50px; |
| | | flex-wrap: wrap; |
| | | justify-content: center; |
| | | margin-top: 80px; |
| | | } |
| | | |
| | | .btn { |
| | | padding: 15px 30px; |
| | | color: white; |
| | | border: none; |
| | | border-radius: 8px; |
| | | cursor: pointer; |
| | | font-size: 16px; |
| | | font-weight: bold; |
| | | transition: all 0.3s ease; |
| | | min-width: 120px; |
| | | |
| | | &:disabled { |
| | | opacity: 0.6; |
| | | cursor: not-allowed; |
| | | } |
| | | } |
| | | |
| | | /* 来电时的全屏样式 */ |
| | | .call-container.incoming-call { |
| | | position: fixed; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | z-index: 99999; |
| | | background-color: #ffffff; |
| | | padding: 0; |
| | | /* 来电时的接听和拒绝按钮样式 */ |
| | | .btn.incoming-accept, .btn.incoming-reject { |
| | | width: 80px; |
| | | height: 80px; |
| | | border-radius: 50%; |
| | | padding: 0; |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); |
| | | } |
| | | |
| | | .btn.incoming-accept { |
| | | background-color: #4CAF50; |
| | | transform: scale(1.1); |
| | | } |
| | | |
| | | .btn.incoming-reject { |
| | | background-color: #f44336; |
| | | } |
| | | |
| | | .btn.incoming-accept:active { |
| | | transform: scale(0.95); |
| | | } |
| | | |
| | | .btn.incoming-reject:active { |
| | | transform: scale(0.95); |
| | | } |
| | | |
| | | .btn-connect { |
| | | background-color: #4CAF50; |
| | | } |
| | | |
| | | .btn-call { |
| | | background-color: #2196F3; |
| | | } |
| | | |
| | | .btn-accept { |
| | | background-color: #4CAF50; |
| | | } |
| | | |
| | | .btn-reject { |
| | | background-color: #f44336; |
| | | } |
| | | |
| | | .btn-hangup { |
| | | background-color: #9E9E9E; |
| | | padding: 15px 40px; |
| | | min-width: 150px; |
| | | } |
| | | |
| | | .status-group { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | align-items: center; |
| | | } |
| | | |
| | | .status-text { |
| | | font-size: 18px; |
| | | color: #333; |
| | | text-align: center; |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | /* 来电状态样式 */ |
| | | .status-text.incoming-status { |
| | | font-size: 24px; |
| | | color: #333; |
| | | font-weight: bold; |
| | | animation: pulse 1.5s infinite; |
| | | } |
| | | |
| | | .status-text.calling-status { |
| | | font-size: 20px; |
| | | color: #666; |
| | | } |
| | | |
| | | /* 呼吸灯动画 */ |
| | | @keyframes pulse { |
| | | 0% { |
| | | opacity: 1; |
| | | transform: scale(1); |
| | | } |
| | | |
| | | .content-wrapper { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 20px; |
| | | max-width: 600px; |
| | | margin: 0 auto; |
| | | height: 100vh; |
| | | justify-content: center; |
| | | align-items: center; |
| | | 50% { |
| | | opacity: 0.7; |
| | | transform: scale(1.05); |
| | | } |
| | | |
| | | .input-group { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | 100% { |
| | | opacity: 1; |
| | | transform: scale(1); |
| | | } |
| | | } |
| | | |
| | | .input-item { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | .tip-message { |
| | | margin-top: 15px; |
| | | color: #666; |
| | | text-align: center; |
| | | } |
| | | |
| | | .log-container { |
| | | margin-top: 20px; |
| | | max-width: 600px; |
| | | margin-left: auto; |
| | | margin-right: auto; |
| | | } |
| | | |
| | | .log-box { |
| | | background-color: white; |
| | | border: 1px solid #ddd; |
| | | border-radius: 8px; |
| | | padding: 10px; |
| | | } |
| | | |
| | | .log-title { |
| | | font-weight: bold; |
| | | color: #333; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .log-content { |
| | | max-height: 200px; |
| | | overflow: auto; |
| | | background-color: #fafafa; |
| | | padding: 10px; |
| | | border-radius: 4px; |
| | | font-size: 12px; |
| | | line-height: 1.4; |
| | | } |
| | | |
| | | .hidden-audio { |
| | | display: none; |
| | | } |
| | | |
| | | /* 头像样式 */ |
| | | .contactAvatar { |
| | | width: 180px; |
| | | height: 180px; |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | margin-bottom: 40px; |
| | | border-radius: 50%; |
| | | background-color: #f0f0f0; |
| | | overflow: hidden; |
| | | box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); |
| | | |
| | | img { |
| | | width: 100%; |
| | | height: 100%; |
| | | object-fit: cover; |
| | | } |
| | | } |
| | | |
| | | .input-label { |
| | | font-weight: bold; |
| | | color: #333; |
| | | } |
| | | |
| | | .input-field { |
| | | width: 120px; |
| | | padding: 8px; |
| | | border: 1px solid #ddd; |
| | | border-radius: 4px; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .button-group { |
| | | display: flex; |
| | | gap: 50px; |
| | | flex-wrap: wrap; |
| | | justify-content: center; |
| | | margin-top: 80px; |
| | | } |
| | | |
| | | .btn { |
| | | padding: 15px 30px; |
| | | color: white; |
| | | border: none; |
| | | border-radius: 8px; |
| | | cursor: pointer; |
| | | font-size: 16px; |
| | | font-weight: bold; |
| | | transition: all 0.3s ease; |
| | | min-width: 120px; |
| | | |
| | | &:disabled { |
| | | opacity: 0.6; |
| | | cursor: not-allowed; |
| | | } |
| | | } |
| | | |
| | | /* 来电时的接听和拒绝按钮样式 */ |
| | | .btn.incoming-accept, .btn.incoming-reject { |
| | | width: 80px; |
| | | height: 80px; |
| | | border-radius: 50%; |
| | | padding: 0; |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); |
| | | } |
| | | |
| | | .btn.incoming-accept { |
| | | background-color: #4CAF50; |
| | | transform: scale(1.1); |
| | | } |
| | | |
| | | .btn.incoming-reject { |
| | | background-color: #f44336; |
| | | } |
| | | |
| | | .btn.incoming-accept:active { |
| | | transform: scale(0.95); |
| | | } |
| | | |
| | | .btn.incoming-reject:active { |
| | | transform: scale(0.95); |
| | | } |
| | | |
| | | .btn-connect { |
| | | background-color: #4CAF50; |
| | | } |
| | | |
| | | .btn-call { |
| | | background-color: #2196F3; |
| | | } |
| | | |
| | | .btn-accept { |
| | | background-color: #4CAF50; |
| | | } |
| | | |
| | | .btn-reject { |
| | | background-color: #f44336; |
| | | } |
| | | |
| | | .btn-hangup { |
| | | background-color: #9E9E9E; |
| | | padding: 15px 40px; |
| | | min-width: 150px; |
| | | } |
| | | |
| | | .status-group { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | align-items: center; |
| | | } |
| | | |
| | | .status-text { |
| | | font-size: 18px; |
| | | color: #333; |
| | | text-align: center; |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | /* 来电状态样式 */ |
| | | .status-text.incoming-status { |
| | | font-size: 24px; |
| | | color: #333; |
| | | font-weight: bold; |
| | | animation: pulse 1.5s infinite; |
| | | } |
| | | |
| | | .status-text.calling-status { |
| | | font-size: 20px; |
| | | color: #666; |
| | | } |
| | | |
| | | /* 呼吸灯动画 */ |
| | | @keyframes pulse { |
| | | 0% { |
| | | opacity: 1; |
| | | transform: scale(1); |
| | | } |
| | | 50% { |
| | | opacity: 0.7; |
| | | transform: scale(1.05); |
| | | } |
| | | 100% { |
| | | opacity: 1; |
| | | transform: scale(1); |
| | | } |
| | | } |
| | | |
| | | .tip-message { |
| | | margin-top: 15px; |
| | | color: #666; |
| | | text-align: center; |
| | | } |
| | | |
| | | .log-container { |
| | | margin-top: 20px; |
| | | max-width: 600px; |
| | | margin-left: auto; |
| | | margin-right: auto; |
| | | } |
| | | |
| | | .log-box { |
| | | background-color: white; |
| | | border: 1px solid #ddd; |
| | | border-radius: 8px; |
| | | padding: 10px; |
| | | } |
| | | |
| | | .log-title { |
| | | font-weight: bold; |
| | | color: #333; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .log-content { |
| | | max-height: 200px; |
| | | overflow: auto; |
| | | background-color: #fafafa; |
| | | padding: 10px; |
| | | border-radius: 4px; |
| | | font-size: 12px; |
| | | line-height: 1.4; |
| | | } |
| | | |
| | | .hidden-audio { |
| | | display: none; |
| | | } |
| | | |
| | | /* 头像样式 */ |
| | | .contactAvatar { |
| | | width: 180px; |
| | | height: 180px; |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | margin-bottom: 40px; |
| | | border-radius: 50%; |
| | | background-color: #f0f0f0; |
| | | overflow: hidden; |
| | | box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); |
| | | |
| | | img { |
| | | width: 100%; |
| | | height: 100%; |
| | | object-fit: cover; |
| | | } |
| | | } |
| | | |
| | | /* 来电时的头像放大效果 */ |
| | | .call-container.incoming-call .contactAvatar { |
| | | width: 220px; |
| | | height: 220px; |
| | | margin-bottom: 60px; |
| | | } |
| | | </style> |
| | | /* 来电时的头像放大效果 */ |
| | | .call-container.incoming-call .contactAvatar { |
| | | width: 220px; |
| | | height: 220px; |
| | | margin-bottom: 60px; |
| | | } |
| | | </style> |
| | |
| | | echarts: |
| | | specifier: 'catalog:' |
| | | version: 5.6.0 |
| | | echarts-gl: |
| | | specifier: ^2.0.9 |
| | | version: 2.0.9(echarts@5.6.0) |
| | | element-plus: |
| | | specifier: 'catalog:' |
| | | version: 2.9.11(vue@3.5.26(typescript@5.9.3)) |
| | |
| | | resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==} |
| | | engines: {node: '>=0.10.0'} |
| | | |
| | | claygl@1.3.0: |
| | | resolution: {integrity: sha512-+gGtJjT6SSHD2l2yC3MCubW/sCV40tZuSs5opdtn79vFSGUgp/lH139RNEQ6Jy078/L0aV8odCw8RSrUcMfLaQ==} |
| | | |
| | | clean-regexp@1.0.0: |
| | | resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} |
| | | engines: {node: '>=4'} |
| | |
| | | |
| | | eastasianwidth@0.2.0: |
| | | resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} |
| | | |
| | | echarts-gl@2.0.9: |
| | | resolution: {integrity: sha512-oKeMdkkkpJGWOzjgZUsF41DOh6cMsyrGGXimbjK2l6Xeq/dBQu4ShG2w2Dzrs/1bD27b2pLTGSaUzouY191gzA==} |
| | | peerDependencies: |
| | | echarts: ^5.1.2 |
| | | |
| | | echarts@5.6.0: |
| | | resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==} |
| | |
| | | isobject: 3.0.1 |
| | | static-extend: 0.1.2 |
| | | |
| | | claygl@1.3.0: {} |
| | | |
| | | clean-regexp@1.0.0: |
| | | dependencies: |
| | | escape-string-regexp: 1.0.5 |
| | |
| | | |
| | | eastasianwidth@0.2.0: {} |
| | | |
| | | echarts-gl@2.0.9(echarts@5.6.0): |
| | | dependencies: |
| | | claygl: 1.3.0 |
| | | echarts: 5.6.0 |
| | | zrender: 5.6.1 |
| | | |
| | | echarts@5.6.0: |
| | | dependencies: |
| | | tslib: 2.3.0 |
| | |
| | | import {useUserStore} from "@/store/index.js"; |
| | | import {getEnvObj} from "@/utils/index.js"; |
| | | // import {enterRoom} from "@/utils/voiceCallByTX/index.js"; |
| | | import { useUserStore } from "@/store/index.js"; |
| | | import { ref, computed, watch } from "vue"; |
| | | import useAppStore from "../store/modules/app/index.js"; |
| | | |
| | | let socketTask = null |
| | | import websocketService from "@/utils/websocket.js"; |
| | | |
| | | export function useGlobalWS() { |
| | | const userStore = useUserStore(); |
| | | const appStore = useAppStore(); |
| | | |
| | | const userId = computed(() => userStore?.userInfo?.user_id) |
| | | const access_token = computed(() => userStore?.userInfo?.access_token) |
| | | const {VITE_APP_WS_API_URL} = getEnvObj() |
| | | // 设置默认用户ID为3 |
| | | const defaultUserId = '3'; |
| | | // WebSocket基础URL |
| | | const WS_BASE = 'wss://wrj.shuixiongit.com/ws/chat?userId='; |
| | | |
| | | // 消息处理 |
| | | function messageHandler(payload) { |
| | | switch (payload.biz_code) { |
| | | case 'JOB_ISREFRESH': |
| | | appStore.setJobUpdateKeyAdd() |
| | | break |
| | | case 'DEVICE_ISREFRESH': |
| | | appStore.setDeviceUpdateKeyAdd() |
| | | break |
| | | case 'DOWNLOAD_PROGRESS': |
| | | break |
| | | case 'LOGOUT_USER': |
| | | userStore.setUserInfo(null) |
| | | uni.reLaunch({ |
| | | url: '/pages/login/index' |
| | | }) |
| | | break |
| | | case 'VoiceCall': |
| | | // enterRoom(payload, userId.value) |
| | | break |
| | | default: |
| | | break; |
| | | } |
| | | } |
| | | function messageHandler(payload) { |
| | | console.log('🌐 全局WebSocket收到消息111111111111111111111:', payload) |
| | | // 先尝试直接处理消息(适用于mobile-web-view的voiceCallDetail页面的消息格式) |
| | | const t = (payload.type || '').toString() |
| | | const bizCode = payload.biz_code || '' |
| | | |
| | | // 关闭ws |
| | | function closeWS() { |
| | | socketTask?.close({ |
| | | success: () => { |
| | | console.log('ws关闭连接'); |
| | | }, |
| | | }) |
| | | } |
| | | console.log('📋 消息分析:type=' + t + ', biz_code=' + bizCode) |
| | | |
| | | // 初始化ws |
| | | function initWS() { |
| | | // 关闭,再连接ws |
| | | closeWS() |
| | | if (!access_token.value) return |
| | | const url = VITE_APP_WS_API_URL |
| | | + `?x-auth-token=${encodeURI(access_token?.value)}` |
| | | + `&model_type=3&workspace-id=${userId.value}` |
| | | // 创建连接 |
| | | socketTask = uni.connectSocket({ |
| | | url: url, |
| | | success: () => { |
| | | console.log('ws连接成功'); |
| | | }, |
| | | fail: (err) => { |
| | | console.error('ws连接失败:', err); |
| | | } |
| | | }); |
| | | // 消息监听 |
| | | socketTask.onMessage((result) => { |
| | | messageHandler(JSON.parse(result.data)) |
| | | }) |
| | | //================================== |
| | | // 监听连接打开 |
| | | socketTask.onOpen((res) => { |
| | | console.log('✅ WebSocket连接已建立') |
| | | // reconnectAttempts = 0 // 连接成功后重置重连次数 |
| | | // 可以在这里发送心跳或订阅消息 |
| | | // startHeartbeat() |
| | | }) |
| | | // 监听连接关闭 |
| | | socketTask.onClose((res) => { |
| | | console.log(`WebSocket连接关闭,代码: ${res.code}, 原因: ${res.reason}`) |
| | | // 处理语音通话请求 |
| | | if (t === 'call') { |
| | | console.log('📞 全局收到来电 call,来自', payload.from) |
| | | // 构建来电参数 |
| | | const callParams = { |
| | | peerUid: payload.from, |
| | | from: payload.from, |
| | | type: 'incoming' |
| | | }; |
| | | |
| | | // 根据不同的关闭代码处理 |
| | | if (res.code === 1000) { // 正常关闭 |
| | | console.log('连接正常关闭') |
| | | } else if (res.code === 1006) { // 异常关闭 |
| | | console.log('连接异常关闭,尝试重连...') |
| | | } else if (res.code === 1011) { // 服务器内部错误 |
| | | console.log('服务器内部错误(1011),延迟重连...') |
| | | } else { |
| | | console.log('其他原因关闭,尝试重连...') |
| | | // 转义参数以便在URL中传递 |
| | | const encodedParams = encodeURIComponent(JSON.stringify(callParams)); |
| | | |
| | | // 跳转到mobile-web-view应用中的语音通话页面 |
| | | const voiceCallUrl = `http://localhost:9527/work-app/#/subPackages/voiceCallDetail/index?voiceparams=${encodedParams}`; |
| | | |
| | | console.log('🔗 准备导航到:', voiceCallUrl) |
| | | |
| | | try { |
| | | // 优先使用uni-app的导航API(适用于uni-app环境) |
| | | if (typeof uni !== 'undefined' && uni.navigateTo) { |
| | | console.log('使用uni-app导航API跳转到语音通话页面'); |
| | | |
| | | // 检查是否是跳转到应用内页面 |
| | | if (voiceCallUrl.includes('/subPackages/voiceCallDetail/')) { |
| | | // 应用内页面跳转 |
| | | uni.navigateTo({ |
| | | url: voiceCallUrl.replace('http://localhost:9527/work-app/#', ''), |
| | | success: () => { |
| | | console.log('✅ 应用内导航成功!'); |
| | | }, |
| | | fail: (err) => { |
| | | console.error('❌ 应用内导航失败:', err); |
| | | // 如果应用内导航失败,尝试使用外部跳转 |
| | | fallbackToExternalNavigation(); |
| | | } |
| | | }); |
| | | } else { |
| | | // 外部页面跳转 |
| | | fallbackToExternalNavigation(); |
| | | } |
| | | } |
| | | // 否则尝试使用window.location跳转(适用于Web环境) |
| | | else if (typeof window !== 'undefined' && window.location) { |
| | | console.log('使用window.location跳转到语音通话页面'); |
| | | window.location.href = voiceCallUrl; |
| | | } else { |
| | | console.error('无法跳转到语音通话页面:当前环境不支持导航'); |
| | | } |
| | | } catch (error) { |
| | | console.error('跳转到语音通话页面失败:', error); |
| | | // 尝试使用外部跳转作为最后的补救措施 |
| | | fallbackToExternalNavigation(); |
| | | } |
| | | }) |
| | | |
| | | // 监听错误 |
| | | socketTask.onError((err) => { |
| | | console.error('WebSocket发生错误:', err) |
| | | }) |
| | | } |
| | | // 外部页面跳转的回调函数 |
| | | function fallbackToExternalNavigation() { |
| | | console.log('尝试使用外部导航跳转到语音通话页面'); |
| | | try { |
| | | // 使用uni-app的外部链接跳转API |
| | | if (typeof uni !== 'undefined' && uni.navigateToWebview) { |
| | | // 这是一个示例,具体API可能需要根据uni-app版本和配置调整 |
| | | uni.navigateToWebview({ |
| | | url: voiceCallUrl, |
| | | success: () => { |
| | | console.log('✅ 外部Webview导航成功!'); |
| | | }, |
| | | fail: (err) => { |
| | | console.error('❌ 外部Webview导航失败:', err); |
| | | // 如果还是失败,尝试使用浏览器打开 |
| | | if (typeof plus !== 'undefined' && plus.runtime) { |
| | | plus.runtime.openURL(voiceCallUrl); |
| | | } else { |
| | | console.error('❌ 所有导航方式都失败了'); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | // 或者使用plus.runtime.openURL(适用于App环境) |
| | | else if (typeof plus !== 'undefined' && plus.runtime) { |
| | | plus.runtime.openURL(voiceCallUrl); |
| | | console.log('尝试使用plus.runtime.openURL打开外部链接'); |
| | | } else { |
| | | console.error('无法跳转到外部页面:当前环境不支持外部导航'); |
| | | } |
| | | } catch (fallbackError) { |
| | | console.error('外部导航尝试失败:', fallbackError); |
| | | } |
| | | } |
| | | return |
| | | } |
| | | |
| | | // 然后处理biz_code格式的消息 |
| | | switch (bizCode) { |
| | | case 'JOB_ISREFRESH': |
| | | appStore.setJobUpdateKeyAdd() |
| | | break |
| | | case 'DEVICE_ISREFRESH': |
| | | appStore.setDeviceUpdateKeyAdd() |
| | | break |
| | | case 'DOWNLOAD_PROGRESS': |
| | | break |
| | | case 'LOGOUT_USER': |
| | | userStore.setUserInfo(null) |
| | | uni.reLaunch({ |
| | | url: '/pages/login/index' |
| | | }) |
| | | break |
| | | case 'VoiceCall': |
| | | // enterRoom(payload, userId.value) |
| | | break |
| | | default: |
| | | // 记录未处理的消息 |
| | | console.log('未处理的WebSocket消息:', payload) |
| | | break; |
| | | } |
| | | } |
| | | |
| | | // watch(access_token, initWS, {immediate: true}) |
| | | // 初始化WebSocket连接 |
| | | function initWS() { |
| | | try { |
| | | // 使用默认用户ID初始化WebSocket连接 |
| | | websocketService.init(defaultUserId, WS_BASE); |
| | | console.log('🌐 全局WebSocket初始化成功,默认用户ID:', defaultUserId); |
| | | |
| | | // 设置全局消息处理回调 |
| | | websocketService.setOnMessageCallback(messageHandler); |
| | | |
| | | console.log('✅ 全局WebSocket消息处理器已设置'); |
| | | } catch (error) { |
| | | console.error('❌ 全局WebSocket初始化失败:', error); |
| | | } |
| | | } |
| | | |
| | | // 监听用户信息变化,初始化WebSocket |
| | | watch( |
| | | () => userStore.userInfo, |
| | | (newUserInfo) => { |
| | | console.log('👤 用户信息变化,尝试初始化WebSocket'); |
| | | initWS(); |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | } |
| | |
| | | { |
| | | "path": "userDetail/infos/index", |
| | | "style": { |
| | | "navigationBarTitleText": "个人资料" |
| | | "navigationBarTitleText": "个人资料", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "userDetail/password/index", |
| | | "style": { |
| | | "navigationBarTitleText": "修改密码" |
| | | "navigationBarTitleText": "修改密码", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | |
| | | import md5 from "js-md5"; |
| | | import { loginByUsername } from "@/api/user/index.js"; |
| | | import { useUserStore } from "@/store/index.js"; |
| | | import websocketService from "@/utils/websocket.js"; |
| | | |
| | | import { HOME_PATH, LOGIN_PATH, removeQueryString } from "@/router"; |
| | | |
| | |
| | | userInfo.key, |
| | | userInfo.code |
| | | ); |
| | | console.log('登录成功,用户信息:', res.data.data); |
| | | userStore.setUserInfo(res.data.data); |
| | | |
| | | |
| | | // 初始化WebSocket连接 |
| | | try { |
| | | const wsUrl = 'wss://wrj.shuixiongit.com/ws/chat?userId='; |
| | | // 设置默认用户ID为3 |
| | | const defaultUserId = '3'; |
| | | // 使用默认用户ID初始化WebSocket连接 |
| | | websocketService.init(defaultUserId, wsUrl); |
| | | console.log('登录成功,WebSocket已初始化,默认用户ID:', defaultUserId); |
| | | |
| | | // 设置来电消息回调 |
| | | websocketService.setOnMessageCallback((message) => { |
| | | if (message.type === 'call') { |
| | | console.log('收到来电,准备跳转到语音通话页面'); |
| | | // 构建来电参数 |
| | | const callParams = { |
| | | peerUid: message.from, |
| | | from: message.from, |
| | | type: 'incoming' |
| | | }; |
| | | |
| | | // 转义参数以便在URL中传递 |
| | | const encodedParams = encodeURIComponent(JSON.stringify(callParams)); |
| | | |
| | | try { |
| | | // 优先使用uni-app的导航API(适用于同应用内跳转) |
| | | if (typeof uni !== 'undefined' && uni.navigateTo) { |
| | | console.log('使用uni-app导航API跳转到语音通话页面'); |
| | | uni.navigateTo({ |
| | | url: `/subPackages/voiceCallDetail/index?params=${encodedParams}` |
| | | }); |
| | | } |
| | | // 否则使用window.location跳转(适用于Web环境) |
| | | else if (typeof window !== 'undefined' && window.location) { |
| | | console.log('使用window.location跳转到语音通话页面'); |
| | | // 注意:实际部署时需要使用正确的URL |
| | | const voiceCallUrl = `http://localhost:8080/appPages/voiceCallDetail/index.html?params=${encodedParams}`; |
| | | window.location.href = voiceCallUrl; |
| | | } else { |
| | | console.error('无法跳转到语音通话页面:当前环境不支持导航'); |
| | | } |
| | | } catch (error) { |
| | | console.error('跳转到语音通话页面失败:', error); |
| | | } |
| | | } |
| | | }); |
| | | } catch (error) { |
| | | console.error('WebSocket初始化失败:', error); |
| | | } |
| | | |
| | | uni.reLaunch({ |
| | | url: "/pages/work/index", |
| | | }); |
| | |
| | | <template> |
| | | <view class="container"> |
| | | <u-navbar |
| | | title="个人资料" |
| | | :is-back="true" |
| | | back-text="" |
| | | :back-icon-size="40" |
| | | @left-click="onBackClick" |
| | | > |
| | | |
| | | </u-navbar> |
| | | <view class="pageBg"></view> |
| | | <div class="avatarBox"> |
| | | <!-- @click="uploadAvatar" --> |
| | |
| | | }); |
| | | |
| | | }; |
| | | // 返回按钮点击事件 |
| | | const onBackClick = () => { |
| | | uni.navigateBack() |
| | | } |
| | | onShow(async () => { |
| | | getUserInfoData() |
| | | }); |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .container { |
| | | width: 100%; |
| | | height: 100%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | position: relative; |
| | | } |
| | | .container { |
| | | width: 100%; |
| | | height: 100%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | position: relative; |
| | | // 添加导航栏高度的padding-top,避免内容被遮挡 |
| | | padding-top: 44px; |
| | | |
| | | } |
| | | :deep(){ |
| | | .u-navbar__content{ |
| | | background: url("@/static/images/user/userbg.svg") no-repeat !important; |
| | | background-size: 100%!important; |
| | | height: 44px !important; |
| | | } |
| | | } |
| | | .pageBg { |
| | | position: fixed; |
| | | top: 0; |
| | |
| | | background: #FFFFFF; |
| | | border-radius: 12rpx; |
| | | box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); |
| | | |
| | | margin-top: 88rpx; |
| | | .detailCon { |
| | | padding: 0 24rpx; |
| | | } |
| | |
| | | <!-- 修改密码 --> |
| | | <template> |
| | | <view class="container"> |
| | | <u-navbar |
| | | title="修改密码" |
| | | :is-back="true" |
| | | back-text="" |
| | | :back-icon-size="40" |
| | | @left-click="onBackClick" |
| | | > |
| | | |
| | | </u-navbar> |
| | | <view class="pageBg"></view> |
| | | <view class="detailBox"> |
| | | <div class="detailCon"> |
| | |
| | | password2: password2, |
| | | code |
| | | }; |
| | | |
| | | |
| | | resetPassword(resetData).then(res => { |
| | | if (res.data.code === 200) { |
| | | uni.showToast({ |
| | |
| | | }); |
| | | }); |
| | | }; |
| | | // 返回按钮点击事件 |
| | | const onBackClick = () => { |
| | | uni.navigateBack() |
| | | } |
| | | onShow(async () => { |
| | | clearForm(); |
| | | }); |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .container { |
| | | width: 100%; |
| | | height: 100%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | position: relative; |
| | | } |
| | | |
| | | .container { |
| | | width: 100%; |
| | | height: 100%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | position: relative; |
| | | // 添加导航栏高度的padding-top,避免内容被遮挡 |
| | | padding-top: 44px; |
| | | |
| | | } |
| | | :deep(){ |
| | | .u-navbar__content{ |
| | | background: url("@/static/images/user/userbg.svg") no-repeat !important; |
| | | background-size: 100%!important; |
| | | height: 44px !important; |
| | | } |
| | | } |
| | | |
| | | .pageBg { |
| | | position: fixed; |
| | | top: 0; |
| | |
| | | background: #FFFFFF; |
| | | border-radius: 12rpx; |
| | | box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); |
| | | |
| | | margin-top: 88rpx; |
| | | .detailCon { |
| | | padding: 0 24rpx; |
| | | } |
| | |
| | | // 构建viewUrl,将contact参数拼接到URL中 |
| | | const contactParam = contact ? encodeURIComponent(JSON.stringify(contact)) : ''; |
| | | // viewUrl.value = `https://192.168.1.157:5176/mobile-web-view/#/webViewWrapper/voiceCallDetail?params=${encodeURIComponent(userParams)}&contact=${contactParam}`; |
| | | viewUrl.value = getWebViewUrl("/voiceCallDetail", { contact: options.contact }); |
| | | viewUrl.value = getWebViewUrl("/voiceCallDetail", { contact: options.contact ,voiceparams: options.voiceparams}); |
| | | }); |
| | | function onPostMessage(data) {} |
| | | // #ifdef APP-PLUS |
| | |
| | | export function getAssetsImage (targetUrl) { |
| | | const url = getEnvObj().VITE_APP_ASSETS_URL |
| | | return `${url}${targetUrl}` |
| | | } |
| | | } |
| New file |
| | |
| | | // WebSocket服务管理 |
| | | class WebSocketService { |
| | | constructor() { |
| | | this.socketTask = null |
| | | this.connected = false |
| | | this.url = '' |
| | | this.userId = '' |
| | | this.onMessageCallback = null |
| | | this.onOpenCallback = null |
| | | this.onCloseCallback = null |
| | | this.onErrorCallback = null |
| | | this.pingTimer = null |
| | | } |
| | | |
| | | // 初始化WebSocket连接 |
| | | init(userId, url) { |
| | | if (!url) { |
| | | console.error('WebSocket初始化失败:缺少url') |
| | | return |
| | | } |
| | | |
| | | this.userId = userId || '' |
| | | this.url = url + encodeURIComponent(this.userId) |
| | | this.connect() |
| | | } |
| | | |
| | | // 建立WebSocket连接 |
| | | connect() { |
| | | try { |
| | | // 关闭现有连接 |
| | | this.close() |
| | | |
| | | // 使用uni-app的WebSocket API |
| | | this.socketTask = uni.connectSocket({ |
| | | url: this.url, |
| | | success: () => { |
| | | console.log('WebSocket连接已请求') |
| | | }, |
| | | fail: (error) => { |
| | | console.error('WebSocket连接请求失败:', error) |
| | | if (this.onErrorCallback) { |
| | | this.onErrorCallback(error) |
| | | } |
| | | } |
| | | }) |
| | | |
| | | // 监听连接打开 |
| | | this.socketTask.onOpen(() => { |
| | | this.connected = true |
| | | console.log('WebSocket已连接,userId:', this.userId) |
| | | this.startPing() |
| | | if (this.onOpenCallback) { |
| | | this.onOpenCallback() |
| | | } |
| | | }) |
| | | |
| | | // 监听连接关闭 |
| | | this.socketTask.onClose(() => { |
| | | this.connected = false |
| | | console.log('WebSocket已关闭') |
| | | this.stopPing() |
| | | if (this.onCloseCallback) { |
| | | this.onCloseCallback() |
| | | } |
| | | }) |
| | | |
| | | // 监听连接错误 |
| | | this.socketTask.onError((error) => { |
| | | console.error('WebSocket错误:', error) |
| | | this.connected = false |
| | | this.stopPing() |
| | | if (this.onErrorCallback) { |
| | | this.onErrorCallback(error) |
| | | } |
| | | }) |
| | | |
| | | // 监听消息 |
| | | this.socketTask.onMessage((res) => { |
| | | try { |
| | | const message = JSON.parse(res.data) |
| | | console.log('收到WebSocket消息:', message) |
| | | if (this.onMessageCallback) { |
| | | this.onMessageCallback(message) |
| | | } |
| | | } catch (error) { |
| | | console.error('解析WebSocket消息失败:', error) |
| | | } |
| | | }) |
| | | } catch (error) { |
| | | console.error('WebSocket连接失败:', error) |
| | | if (this.onErrorCallback) { |
| | | this.onErrorCallback(error) |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 发送消息 |
| | | send(type, to, payload) { |
| | | if (!type || !this.socketTask || !this.connected) { |
| | | console.error('WebSocket发送失败:连接未建立') |
| | | return |
| | | } |
| | | |
| | | const message = { |
| | | type, |
| | | from: String(this.userId), |
| | | to: String(to), |
| | | payload: payload || null, |
| | | timestamp: Date.now() |
| | | } |
| | | |
| | | this.socketTask.send({ |
| | | data: JSON.stringify(message), |
| | | success: () => { |
| | | console.log('WebSocket消息发送成功:', message) |
| | | }, |
| | | fail: (error) => { |
| | | console.error('WebSocket消息发送失败:', error) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | // 开始心跳检测 |
| | | startPing() { |
| | | this.stopPing() |
| | | // 每25秒发送一次心跳 |
| | | this.pingTimer = setInterval(() => { |
| | | if (this.connected) { |
| | | this.send('ping', 'system') |
| | | } |
| | | }, 25000) |
| | | } |
| | | |
| | | // 停止心跳检测 |
| | | stopPing() { |
| | | if (this.pingTimer) { |
| | | clearInterval(this.pingTimer) |
| | | this.pingTimer = null |
| | | } |
| | | } |
| | | |
| | | // 关闭连接 |
| | | close() { |
| | | if (this.socketTask) { |
| | | this.socketTask.close({ |
| | | code: 1000, |
| | | reason: '正常关闭', |
| | | success: () => { |
| | | console.log('WebSocket已关闭') |
| | | }, |
| | | fail: (error) => { |
| | | console.error('WebSocket关闭失败:', error) |
| | | } |
| | | }) |
| | | this.socketTask = null |
| | | } |
| | | this.stopPing() |
| | | this.connected = false |
| | | } |
| | | |
| | | // 设置消息回调 |
| | | setOnMessageCallback(callback) { |
| | | this.onMessageCallback = callback |
| | | } |
| | | |
| | | // 设置连接打开回调 |
| | | setOnOpenCallback(callback) { |
| | | this.onOpenCallback = callback |
| | | } |
| | | |
| | | // 设置连接关闭回调 |
| | | setOnCloseCallback(callback) { |
| | | this.onCloseCallback = callback |
| | | } |
| | | |
| | | // 设置错误回调 |
| | | setOnErrorCallback(callback) { |
| | | this.onErrorCallback = callback |
| | | } |
| | | |
| | | // 获取连接状态 |
| | | getConnected() { |
| | | return this.connected |
| | | } |
| | | } |
| | | |
| | | // 导出单例实例 |
| | | export default new WebSocketService() |
| | |
| | | url: `/system/sys/SysRegionCode/allTree`, |
| | | method: 'get' |
| | | }) |
| | | } |
| | | } |
| | |
| | | data: data, |
| | | }); |
| | | }; |
| | | |
| | | // 验证码 |
| | | export const registerSendCodeApi = (phone) => { |
| | | return request({ |
| | | url: `/system/verificationCode/registerSendCode/${phone}`, |
| | | method: 'get' |
| | | }) |
| | | } |
| | |
| | | <template> |
| | | <view class="equipmentManagement"> |
| | | <u-search :bgColor="'white'" v-model="noticeTitle" :show-action="true" actionText="搜索" :animation="true" @custom="getEquipmentList"></u-search> |
| | | <u-search :bgColor="'white'" v-model="noticeTitle" :show-action="true" actionText="搜索" :animation="true" @custom="handleSearch"></u-search> |
| | | <view class="content"> |
| | | <view class="title">飞行设备</view> |
| | | <view class="card" v-for="(item, index) in equipmentList" :key="index"> |
| | | <view class="left"> |
| | | <u-image :src="droneSvg" mode="aspectFill" :width="70" :height="90"></u-image> |
| | | <u-list :style="{ height: 'calc(100vh - 160rpx)' }" @scrolltolower="scrolltolower" :lower-threshold="50"> |
| | | <view class="card" v-for="(item, index) in equipmentList" :key="index"> |
| | | <view class="left"> |
| | | <u-image :src="droneSvg" mode="aspectFill" :width="70" :height="90"></u-image> |
| | | </view> |
| | | <view class="right"> |
| | | <view class="name">设备名称:{{ item.name }}</view> |
| | | <view class="txt">设备类型:{{ item.type }}</view> |
| | | <view class="txt">设备型号:{{ item.type }}</view> |
| | | <view class="txt">设备sn:{{ item.sn }}</view> |
| | | </view> |
| | | </view> |
| | | <view class="right"> |
| | | <view class="name">设备名称:{{ item.name }}</view> |
| | | <view class="txt">设备类型:{{ item.type }}</view> |
| | | <view class="txt">设备型号:{{ item.type }}</view> |
| | | <view class="txt">设备sn:{{ item.sn }}</view> |
| | | </view> |
| | | </view> |
| | | </u-list> |
| | | </view> |
| | | <view class="add-btn" @click="addEquipment"> |
| | | <u-image :src="addSvg" mode="aspectFill" :width="60" :height="68"></u-image> |
| | |
| | | import { myDevicePageInfoApi, aircraftInfoPageInfoOfMyApi } from '@/api/index.js' |
| | | const equipmentList = ref([]) |
| | | const noticeTitle = ref('') |
| | | const formParams = ref({ |
| | | current: 1, |
| | | size: 10, |
| | | model:'', |
| | | name: '', |
| | | }) |
| | | // 搜索处理 |
| | | const handleSearch = () => { |
| | | // 搜索时重置为第一页 |
| | | formParams.value.current = 1 |
| | | formParams.value.name = noticeTitle.value |
| | | getEquipmentList() |
| | | } |
| | | |
| | | // 设备列表 |
| | | const getEquipmentList = async () => { |
| | | try { |
| | | const res = await aircraftInfoPageInfoOfMyApi({ |
| | | current: 1, |
| | | size: 99, |
| | | model:'', |
| | | name: '', |
| | | }) |
| | | equipmentList.value = res.data.data.records || [] |
| | | const res = await aircraftInfoPageInfoOfMyApi(formParams.value) |
| | | const newData = res.data.data.records || [] |
| | | // 如果是第一页,直接替换数据;否则追加数据 |
| | | if (formParams.value.current === 1) { |
| | | equipmentList.value = newData |
| | | } else { |
| | | equipmentList.value = equipmentList.value.concat(newData) |
| | | } |
| | | } catch (error) { |
| | | console.error('获取设备列表失败:', error) |
| | | } |
| | |
| | | url: '/subPackages/deviceRegistration/add', |
| | | }) |
| | | } |
| | | function scrolltolower() { |
| | | formParams.value.current++ |
| | | // 停止不刷新 |
| | | if (equipmentList.value.length < formParams.value.size) { |
| | | return |
| | | } |
| | | getEquipmentList() |
| | | } |
| | | onMounted(() => { |
| | | getEquipmentList() |
| | | }) |
| | |
| | | <template> |
| | | <view class="legalPolicy"> |
| | | <!-- <u-navbar title="法律政策" @rightClick="rightClick" :autoBack="true"></u-navbar> --> |
| | | <u-search :bgColor="'white'" v-model="noticeTitle" :show-action="true" actionText="搜索" :animation="true" @custom="getSysNoticeList"></u-search> |
| | | <u-search :bgColor="'white'" v-model="noticeTitle" :show-action="true" actionText="搜索" :animation="true" @custom="handleSearch"></u-search> |
| | | <view class="tabs-container"> |
| | | <u-tabs :list="tabs" @click="tabClick"></u-tabs> |
| | | </view> |
| | | <view class="content"> |
| | | <view class="card" v-for="(item, index) in announcementList" :key="index" @click="toRegulationsDetail(item)"> |
| | | <view class="left"> |
| | | <u-image :src="docSvg" mode="aspectFill" :width="30" :height="36"></u-image> |
| | | <u-list :style="{ height: 'calc(100vh - 160rpx)' }" @scrolltolower="scrolltolower" :lower-threshold="50"> |
| | | <view class="card" v-for="(item, index) in announcementList" :key="index" @click="toRegulationsDetail(item)"> |
| | | <view class="left"> |
| | | <u-image :src="docSvg" mode="aspectFill" :width="30" :height="36"></u-image> |
| | | </view> |
| | | <view class="right"> |
| | | <view class="r-top">{{ item.title }}</view> |
| | | <view class="r-middle">{{ item.description || '暂无描述' }}</view> |
| | | <view class="r-bottom">{{ item.time || '暂无' }}</view> |
| | | </view> |
| | | </view> |
| | | <view class="right"> |
| | | <view class="r-top">{{ item.title }}</view> |
| | | <view class="r-middle">{{ item.description || '暂无描述' }}</view> |
| | | <view class="r-bottom">{{ item.time || '暂无' }}</view> |
| | | </view> |
| | | </view> |
| | | </u-list> |
| | | </view> |
| | | </view> |
| | | </template> |
| | |
| | | }, |
| | | ]) |
| | | |
| | | const currentTab = ref(2) |
| | | const formParams = ref({ |
| | | current: 1, |
| | | size: 10, |
| | | noticeType: 2, |
| | | noticeTitle: noticeTitle.value, |
| | | }) |
| | | |
| | | const tabClick = (val) => { |
| | | if (val.index === 0) { |
| | | currentTab.value = 2 |
| | | getSysNoticeList() |
| | | formParams.value.noticeType = 2 |
| | | } else { |
| | | currentTab.value = 1 |
| | | getSysNoticeList() |
| | | } |
| | | formParams.value.noticeType = 1 |
| | | } |
| | | formParams.value.current = 1 |
| | | announcementList.value = [] |
| | | getSysNoticeList() |
| | | } |
| | | |
| | | const announcementList = ref() |
| | | |
| | | // 搜索处理 |
| | | const handleSearch = () => { |
| | | // 搜索时重置为第一页 |
| | | formParams.value.current = 1 |
| | | formParams.value.noticeTitle = formParams.value.noticeTitle.trim() |
| | | getSysNoticeList() |
| | | } |
| | | // 触底加载 |
| | | function scrolltolower() { |
| | | formParams.value.current++ |
| | | // 停止不刷新 |
| | | if (formParams.value.current > formParams.value.totalPages) { |
| | | uni.showToast({ |
| | | title: '没有更多了', |
| | | icon: 'none', |
| | | duration: 2000, |
| | | }) |
| | | return |
| | | } |
| | | getSysNoticeList() |
| | | } |
| | | // 获取通知公告列表 |
| | | async function getSysNoticeList() { |
| | | try { |
| | | const res = await sysNoticePageInfoApi({ |
| | | current: 1, |
| | | size: 10, |
| | | noticeType: currentTab.value, |
| | | noticeTitle: noticeTitle.value, |
| | | }) |
| | | announcementList.value = res.data.data.records.map(item => ({ |
| | | const res = await sysNoticePageInfoApi(formParams.value) |
| | | const newData = res.data.data.records.map(item => ({ |
| | | ...item, |
| | | title: item.noticeTitle, |
| | | description: item.remark, |
| | |
| | | num: item.num, |
| | | like: item.like, |
| | | })) |
| | | // 如果是第一页,直接替换数据;否则追加数据 |
| | | if (formParams.value.current === 1) { |
| | | announcementList.value = newData |
| | | } else { |
| | | announcementList.value = announcementList.value.concat(newData) |
| | | } |
| | | } catch (error) { |
| | | uni.showToast({ |
| | | title: error.message, |
| | |
| | | url: '/subPackages/regulationsDetail/details?id=' + item.noticeId+'&type=2', |
| | | }) |
| | | } |
| | | |
| | | |
| | | |
| | | onMounted(() => { |
| | | getSysNoticeList() |
| | |
| | | <div class="title">低空经济服务一体化平台系统</div> |
| | | <view class="user-password"> |
| | | <div class="user-name"> |
| | | <input v-model="loginForm.username" placeholder="请输入用户名" /> |
| | | <u-input border="none" v-model="loginForm.username" placeholder="请输入用户名"></u-input> |
| | | </div> |
| | | <div class="pass-word"> |
| | | <u-input type="password" border="bottom" v-model="loginForm.password" placeholder="请输入密码"></u-input> |
| | | <u-input border="none" type="password" v-model="loginForm.password" placeholder="请输入密码"></u-input> |
| | | <!-- <input v-model="loginForm.password" :type="isShowPassword ? 'text' : 'password'" placeholder="请输入密码"/> |
| | | <image v-if="!isShowPassword" :src="showPasswordSvg" @click="isShowPassword = !isShowPassword" /> |
| | | <image v-else :src="showPsdSvg" @click="isShowPassword = !isShowPassword" /> --> |
| | |
| | | </view> |
| | | </view> |
| | | <view class="row"> |
| | | <view class="label">昵称</view> |
| | | <view class="label"><span class="nike"></span>昵称</view> |
| | | <view class="input"> |
| | | <u-input input-align="right" border="bottom" v-model="formParams.nikeName" placeholder="昵称"></u-input> |
| | | </view> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { sendResetCode, registerUser } from '@/api/user/index.js'; |
| | | import { sendResetCode, registerSendCodeApi, registerUser } from '@/api/user/index.js'; |
| | | const formParams = ref({ |
| | | userName: '', |
| | | nikeName: '', |
| | |
| | | } |
| | | |
| | | // 这里假设账号就是手机号,实际项目中可能需要验证格式 |
| | | sendResetCode(formParams.value.userName).then(res => { |
| | | console.log('4444',res) |
| | | registerSendCodeApi(formParams.value.userName).then(res => { |
| | | if (res.data.code === 200) { |
| | | uni.showToast({ |
| | | title: '验证码发送成功', |
| | |
| | | title: '注册成功', |
| | | icon: 'success', |
| | | }); |
| | | // 跳转登录页面 |
| | | uni.navigateTo({ |
| | | url: '/pages/login/index' |
| | | }) |
| | | } else { |
| | | uni.showToast({ |
| | | title: res.data.msg || '注册失败', |
| | |
| | | left: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | background-image: url("../../static/images/user/userbg.svg") no-repeat ; |
| | | background-image: url("https://wrj.shuixiongit.com/aiskyminio/cloud-bucket/ja-app-wx/images/user/user-bg.png") no-repeat ; |
| | | background-size: 100%; |
| | | } |
| | | .content { |
| | |
| | | font-weight: 400; |
| | | font-size: 15px; |
| | | color: #222324; |
| | | .nike { |
| | | margin-right: 16rpx; |
| | | } |
| | | .required { |
| | | color: #FF4444; |
| | | } |
| | |
| | | <!-- 我的 --> |
| | | <template> |
| | | <view class="page-wrap"> |
| | | <view class="pageUser"> |
| | | <view class="userBox"> |
| | | <view class="flex items-center pb-30rpx pl-30rpx pr-20rpx"> |
| | | <view class="mr-20rpx"> |
| | | <u-avatar @click="uploadAvatar" :src="user.avatar || showDefaultHeader" size="70" /> |
| | | </view> |
| | | <view class="flex-1"> |
| | | <view class="userName">{{user.nickName }}</view> |
| | | <view class="departs"> |
| | | <image |
| | | src="@/static/images/user/department.svg" |
| | | alt="" |
| | | /> |
| | | <span>{{ user?.sysDept?.deptName }}</span> |
| | | </view> |
| | | </view> |
| | | |
| | | </view> |
| | | <view class="personalInformation"> |
| | | <div class="message"> |
| | | <div class="messagebox">个人资料</div> |
| | | <div class="editInfo" @click="handelEdit(1)"> |
| | | 点击编辑 |
| | | <image :src="rightImage" alt="" /> |
| | | </div> |
| | | </div> |
| | | <div class="passwordBox"> |
| | | <div class="messagebox">修改密码</div> |
| | | <div class="editInfo" @click="handelEdit(2)"> |
| | | 点击编辑 |
| | | <image :src="rightImage" alt="" /> |
| | | </div> |
| | | </div> |
| | | </view> |
| | | |
| | | <view class="goOutBtn"> |
| | | <div class="goOutStyle" @click="logOut">退出登录</div> |
| | | </view> |
| | | |
| | | </view> |
| | | |
| | | </view> |
| | | <view class="pageUser"> |
| | | <view class="userBox"> |
| | | <view class="flex items-center pb-30rpx pl-30rpx pr-20rpx"> |
| | | <view class="mr-20rpx"> |
| | | <u-avatar @click="uploadAvatar" :src="user.avatar || showDefaultHeader" size="70" /> |
| | | </view> |
| | | <view class="flex-1"> |
| | | <view class="userName">{{user.nickName }}</view> |
| | | <view class="departs"> |
| | | <image |
| | | src="@/static/images/user/department.png" |
| | | alt="" |
| | | /> |
| | | <span>{{ user?.sysDept?.deptName }}</span> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view class="personalInformation"> |
| | | <div class="message"> |
| | | <div class="messagebox">个人资料</div> |
| | | <div class="editInfo" @click="handelEdit(1)"> |
| | | 点击编辑 |
| | | <image :src="rightImage" alt="" /> |
| | | </div> |
| | | </div> |
| | | <div class="passwordBox"> |
| | | <div class="messagebox">修改密码</div> |
| | | <div class="editInfo" @click="handelEdit(2)"> |
| | | 点击编辑 |
| | | <image :src="rightImage" alt="" /> |
| | | </div> |
| | | </div> |
| | | </view> |
| | | |
| | | <view class="goOutBtn"> |
| | | <div class="goOutStyle" @click="logOut">退出登录</div> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import { getDeviceRegionApi } from "@/api/map.js"; |
| | | import defaultAvatar from '/static/images/defaultAvatar.svg' |
| | | import rightImage from '@/static/images/user/rightBtn.svg'; |
| | | import rightImage from '@/static/images/user/rightBtn.png'; |
| | | import showDefaultHeader from "@/static/images/user/default-header.png"; |
| | | const { setClipboardData, getClipboardData } = useClipboard(); |
| | | // |
| | |
| | | height: 100%; |
| | | } |
| | | .pageUser { |
| | | background-image: url("../../static/images/user/userbg.svg") no-repeat; |
| | | background: url("https://wrj.shuixiongit.com/aiskyminio/cloud-bucket/ja-app-wx/images/user/user-bg.png") no-repeat ; |
| | | background-size: 100%; |
| | | } |
| | | .userBox { |
| | |
| | | } |
| | | |
| | | .message { |
| | | background: url('@/static/images/user/info.svg') |
| | | background: url('@/static/images/user/info.png') |
| | | no-repeat center; |
| | | background-size: 100% 100%; |
| | | } |
| | | |
| | | .passwordBox { |
| | | background: url('@/static/images/user/password.svg') |
| | | background: url('@/static/images/user/password.png') |
| | | no-repeat center; |
| | | background-size: 100% 100%; |
| | | } |
| | |
| | | <u-form-item |
| | | label="所属区域" |
| | | labelWidth="230rpx" |
| | | prop="serialNumber" |
| | | prop="regionCode" |
| | | :borderBottom="true" |
| | | ref="item2" |
| | | @click="isShowRegion = true" |
| | | > |
| | | <view class="select-wrapper"> |
| | | <u-input |
| | | v-model="formParams.serialNumber" |
| | | border="none" |
| | | placeholder="请选择" |
| | | readonly |
| | | ></u-input> |
| | | <view class="select-icon">▼</view> |
| | | </view> |
| | | <u-input |
| | | v-model="formParams.regionText" |
| | | disabled |
| | | disabledColor="#ffffff" |
| | | placeholder="请选择所属区域" |
| | | border="none" |
| | | ></u-input> |
| | | <template #right> |
| | | <up-icon name="arrow-down"></up-icon> |
| | | </template> |
| | | </u-form-item> |
| | | <u-form-item |
| | | label="民用航空器登记证书" |
| | |
| | | /> |
| | | <!-- 所属区域 --> |
| | | <u-cascader |
| | | show="show" |
| | | v-model="value" |
| | | :show="isShowRegion" |
| | | v-model="formParams.regionCode" |
| | | :data="areaData" |
| | | value-key="code" |
| | | label-key="name" |
| | | children-key="children" |
| | | :auto-close="true" |
| | | @confirm="onRegionConfirm" |
| | | @close="isShowRegion = false" |
| | | @cancel="isShowRegion = false" |
| | | ></u-cascader> |
| | | </u-form> |
| | | <u-button @click="submitForm" color="#1D6FE9">提交</u-button> |
| | |
| | | hasInsurance: '', // 是否有保险 |
| | | hasInsuranceText: '', // 是否有保险文本 |
| | | region: '', // 所属区域 |
| | | regionCode: '', // 所属区域 |
| | | regionText: '', // 所属区域文本 |
| | | caacRegistrationCode: '', // 民用航空器登记证书 |
| | | caacRegistrationCodeText: '', // 民用航空器登记证书文本 |
| | | deviceImage: '', // 飞行器照片 |
| | |
| | | const onPurchaseDate = (e) => { |
| | | formParams.value.purchaseDate = dayjs(e.value).format('YYYY-MM-DD') |
| | | showPurchaseDate.value = false |
| | | } |
| | | |
| | | const findRegionNamesByCodes = (codes, areaData) => { |
| | | const result = [] |
| | | let currentData = areaData |
| | | |
| | | for (const code of codes) { |
| | | // 转换为字符串比较(因为code可能是字符串类型) |
| | | const codeStr = code.toString() |
| | | const item = currentData.find(item => item.code === codeStr) |
| | | |
| | | if (!item) { |
| | | console.warn(`未找到code为 ${codeStr} 的行政区划`) |
| | | break |
| | | } |
| | | |
| | | result.push(item.name) |
| | | currentData = item.children || [] |
| | | } |
| | | |
| | | return result |
| | | } |
| | | |
| | | // 所属区域确认 |
| | | const onRegionConfirm = (e) => { |
| | | console.log(e,'888') |
| | | formParams.value.region = e?.join(',')[2] || '' // 用逗号分隔的区域码 |
| | | // 通过arrRegion的数组值为[36,3608,360824] 从areaData里面反查出名称 |
| | | const regionNames = findRegionNamesByCodes(e, areaData.value) |
| | | console.log(regionNames, '77777') |
| | | formParams.value.regionText = regionNames.join('-') || '' // 用连字符连接的区域名称 |
| | | isShowRegion.value = false |
| | | } |
| | | // 是否有保险 |
| | | const actionsHasInsurance = ref([ |
| | |
| | | // 获取所属单位 |
| | | const isShowRegion = ref(false) |
| | | const areaData = ref([]) |
| | | function getAreaData() { |
| | | areaDataApi().then(res => { |
| | | if (res.data.code === 200) { |
| | | areaData.value = res.data.data |
| | | } |
| | | }) |
| | | } |
| | | // function getAreaData() { |
| | | // areaDataApi().then(res => { |
| | | // if (res.data.code === 200) { |
| | | // areaData.value = res.data.data |
| | | // } |
| | | // }) |
| | | // } |
| | | // 提交图片 |
| | | function afterReadImage(event) { |
| | | // 获取上传的文件对象 |
| | |
| | | // 这里可以添加额外的失败处理逻辑 |
| | | } |
| | | } |
| | | onMounted(() => { |
| | | getAreaData() |
| | | onMounted(async () => { |
| | | // 动态导入xzqhData,确保在使用前已初始化 |
| | | const { xzqhData } = await import('@/static/xzqh/index') |
| | | // console.log(xzqhData,'9999') |
| | | // xzqhData.map 循环三层 |
| | | // 第一层:省份 |
| | | // 第二层:城市 |
| | | // 第三层:区县 |
| | | areaData.value = xzqhData |
| | | // xzqhData.map(item => ({ |
| | | // label: item.name, |
| | | // value: item.code, |
| | | // children: item.children.map(child => ({ |
| | | // label: child.name, |
| | | // value: child.code, |
| | | // children: child.children?.map(district => ({ |
| | | // label: district.name, |
| | | // value: district.code |
| | | // })) || [] |
| | | // })) |
| | | // })) |
| | | // console.log(areaData.value, '8888') |
| | | |
| | | }) |
| | | </script> |
| | | <style scoped lang="scss"> |
| | |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <u-search :bgColor="'white'" :show-action="true" actionText="搜索" :animation="true"></u-search> |
| | | <u-search v-model="formParams.flightPlanName" :bgColor="'white'" :show-action="true" actionText="搜索" :animation="true" @custom="handleSearch"></u-search> |
| | | <view class="content"> |
| | | <view class="sp-title"> |
| | | 审批记录 |
| | | </view> |
| | | <view class="approvalRecord" v-for="(approvalRecord,approvalRecordIndex) in approvalRecords" :key="approvalRecordIndex"> |
| | | <view class="name-status"> |
| | | <view class="name-image"> |
| | | <u-image :src="spSvg" class="icon" width="16" height="16"></u-image> |
| | | <view class="name">{{approvalRecord.flightPlanName}}</view> |
| | | <u-list :style="{ height: 'calc(100vh - 240rpx)' }" @scrolltolower="scrolltolower" :lower-threshold="50"> |
| | | <view class="approvalRecord" v-for="(approvalRecord,approvalRecordIndex) in approvalRecords" :key="approvalRecordIndex"> |
| | | <view class="name-status"> |
| | | <view class="name-image"> |
| | | <u-image :src="spSvg" class="icon" width="16" height="16"></u-image> |
| | | <view class="name">{{approvalRecord.flightPlanName}}</view> |
| | | </view> |
| | | <view class="status">{{approvalRecord.planStatus || '无状态'}}</view> |
| | | </view> |
| | | <view class="status">{{approvalRecord.planStatus || '无状态'}}</view> |
| | | <view class="txt">飞行设备:{{approvalRecord.device || '无设备'}}</view> |
| | | <view class="txt">行政区划:{{approvalRecord.area || '无行政区划'}}</view> |
| | | <view class="txt">飞行时间:{{approvalRecord.time || '无飞行时间'}}</view> |
| | | </view> |
| | | <view class="txt">飞行设备:{{approvalRecord.device || '无设备'}}</view> |
| | | <view class="txt">行政区划:{{approvalRecord.area || '无行政区划'}}</view> |
| | | <view class="txt">飞行时间:{{approvalRecord.time || '无飞行时间'}}</view> |
| | | </view> |
| | | </u-list> |
| | | </view> |
| | | </view> |
| | | </template> |
| | |
| | | } |
| | | |
| | | // 审批记录是很多条 |
| | | const approvalRecords = ref([ |
| | | { |
| | | flightPlanName: '飞行需求名称', |
| | | planStatus: '申请中', |
| | | device: '大疆无人机', |
| | | area: '吉州区', |
| | | time: '2026/01/07至2026/01/08', |
| | | }, |
| | | { |
| | | flightPlanName: '飞行需求名称', |
| | | planStatus: '申请中', |
| | | device: '大疆无人机', |
| | | area: '吉州区', |
| | | time: '2026/01/07至2026/01/08', |
| | | }, |
| | | { |
| | | flightPlanName: '飞行需求名称', |
| | | planStatus: '申请中', |
| | | device: '大疆无人机', |
| | | area: '吉州区', |
| | | time: '2026/01/07至2026/01/08', |
| | | }, |
| | | { |
| | | flightPlanName: '飞行需求名称', |
| | | planStatus: '申请中', |
| | | device: '大疆无人机', |
| | | area: '吉州区', |
| | | time: '2026/01/07至2026/01/08', |
| | | }, |
| | | { |
| | | flightPlanName: '飞行需求名称', |
| | | planStatus: '申请中', |
| | | device: '大疆无人机', |
| | | area: '吉州区', |
| | | time: '2026/01/07至2026/01/08', |
| | | }, |
| | | { |
| | | flightPlanName: '飞行需求名称', |
| | | planStatus: '申请中', |
| | | device: '大疆无人机', |
| | | area: '吉州区', |
| | | time: '2026/01/07至2026/01/08', |
| | | }, |
| | | { |
| | | flightPlanName: '飞行需求名称', |
| | | planStatus: '申请中', |
| | | device: '大疆无人机', |
| | | area: '吉州区', |
| | | time: '2026/01/07至2026/01/08', |
| | | }, |
| | | ]) |
| | | const approvalRecords = ref([]) |
| | | const formParams = ref({ |
| | | current: 1, |
| | | size: 10, |
| | | flightPlanName: '', |
| | | }) |
| | | // 申请审批列表 |
| | | const getApprovalRecords = async () => { |
| | | try { |
| | | const res = await flightPlanPageInfoApi({ |
| | | current: 1, |
| | | size: 10, |
| | | }) |
| | | approvalRecords.value = res.data.data.records || [] |
| | | const res = await flightPlanPageInfoApi(formParams.value) |
| | | const newData = res.data.data.records || [] |
| | | // 如果是第一页,直接替换数据;否则追加数据 |
| | | if (formParams.value.current === 1) { |
| | | approvalRecords.value = newData |
| | | } else { |
| | | approvalRecords.value = approvalRecords.value.concat(newData) |
| | | } |
| | | } catch (error) { |
| | | uni.showToast({ |
| | | title: error.message, |
| | |
| | | duration: 2000, |
| | | }) |
| | | } |
| | | } |
| | | // 搜索处理 |
| | | const handleSearch = () => { |
| | | // 搜索时重置为第一页 |
| | | formParams.value.current = 1 |
| | | formParams.value.flightPlanName = formParams.value.flightPlanName.trim() |
| | | getApprovalRecords() |
| | | } |
| | | // 触底加载 |
| | | function scrolltolower() { |
| | | formParams.value.current++ |
| | | // 停止不刷新 |
| | | if (approvalRecords.value.length < formParams.value.size) { |
| | | return |
| | | } |
| | | getApprovalRecords() |
| | | } |
| | | onMounted(() => { |
| | | getApprovalRecords() |
| | |
| | | .flightApplication { |
| | | .header-top { |
| | | width: 100%; |
| | | height: 350rpx; |
| | | background: url('@/static/images/fxsq-bg.png') no-repeat center; |
| | | background-size: 100% 100%; |
| | | height: 280rpx; |
| | | background: url("https://wrj.shuixiongit.com/aiskyminio/cloud-bucket/ja-app-wx/images/fxsq-bg.png") no-repeat ; |
| | | background-size: 100%; |
| | | position: fixed; |
| | | top: 0; |
| | | .ht-content { |
| | |
| | | width: 90%; |
| | | left: 50%; |
| | | transform: translateX(-50%); |
| | | top: 340rpx !important; |
| | | top: 350rpx !important; |
| | | height: 68rpx; |
| | | flex: 0; |
| | | } |
| | |
| | | } |
| | | } |
| | | .fj-list { |
| | | margin-top: 40rpx; |
| | | // margin-top: 20rpx; |
| | | } |
| | | .fj-item { |
| | | display: flex; |
| | |
| | | left: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | background-image: url("../../../static/images/user/userbg.svg") no-repeat ; |
| | | background-size: 100%; |
| | | background: url("https://wrj.shuixiongit.com/aiskyminio/cloud-bucket/ja-app-wx/images/user/user-bg.png") no-repeat ; |
| | | background-size: 100%; |
| | | z-index: -1; |
| | | } |
| | | .avatarBox { |
| | | width: 228rpx; |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import userbgSvg from "@/static/images/user/userbg.svg"; |
| | | import userBgPng from "@/static/images/user/user-bg.png"; |
| | | import { |
| | | useUserStore |
| | | } from "@/store/index.js"; |
| | |
| | | left: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | background-image: url("https://wrj.shuixiongit.com/aiskyminio/cloud-bucket/ztzf_app_assets/images/user/bg.png"); |
| | | background-size: 100%; |
| | | background: url("https://wrj.shuixiongit.com/aiskyminio/cloud-bucket/ja-app-wx/images/user/user-bg.png") no-repeat ; |
| | | background-size: 100%; |
| | | } |
| | | |
| | | .detailBox { |
| | |
| | | font-weight: 400; |
| | | font-size: 30rpx; |
| | | color: #222324; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | input.input-item { |