吉安感知网项目-前端
shuishen
2026-01-15 391b8fe68a295522915bc734d5032d64a3781b35
Merge branch 'master' of http://139.196.74.78:10010/r/jagzwxm/ja_web
25 files modified
6 files added
7 files deleted
3242 ■■■■ changed files
applications/drone-command/package.json 11 ●●●● patch | view | raw | blame | history
applications/drone-command/src/assets/images/basicManage/chartBg.svg 132 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/assets/images/basicManage/chartBg2.svg 132 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/basicManage/deviceStock/DeviceChart1.vue 263 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/basicManage/deviceStock/DeviceChart2.vue 156 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/basicManage/deviceStock/DeviceChart3.vue 145 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/basicManage/deviceStock/fwDevice.js 25 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/basicManage/deviceStock/index.vue 46 ●●●●● patch | view | raw | blame | history
applications/mobile-web-view/src/appPages/voiceCallDetail/index.vue 1109 ●●●● patch | view | raw | blame | history
pnpm-lock.yaml 19 ●●●●● patch | view | raw | blame | history
uniapps/work-app/src/hooks/useGlobalWS.js 239 ●●●●● patch | view | raw | blame | history
uniapps/work-app/src/pages.json 6 ●●●●● patch | view | raw | blame | history
uniapps/work-app/src/pages/login/index.vue 53 ●●●●● patch | view | raw | blame | history
uniapps/work-app/src/subPackages/userDetail/infos/index.vue 41 ●●●● patch | view | raw | blame | history
uniapps/work-app/src/subPackages/userDetail/password/index.vue 45 ●●●● patch | view | raw | blame | history
uniapps/work-app/src/subPackages/voiceCallDetail/index.vue 2 ●●● patch | view | raw | blame | history
uniapps/work-app/src/utils/common/index.js 2 ●●● patch | view | raw | blame | history
uniapps/work-app/src/utils/websocket.js 188 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/api/index.js 2 ●●● patch | view | raw | blame | history
uniapps/work-wx/src/api/user/index.js 8 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/pages/equipmentManagement/index.vue 60 ●●●● patch | view | raw | blame | history
uniapps/work-wx/src/pages/legalPolicy/index.vue 79 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/pages/login/index.vue 4 ●●●● patch | view | raw | blame | history
uniapps/work-wx/src/pages/register/index.vue 16 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/pages/user/index.vue 89 ●●●● patch | view | raw | blame | history
uniapps/work-wx/src/static/images/user/default-header.png patch | view | raw | blame | history
uniapps/work-wx/src/static/images/user/default-header.svg 17 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/static/images/user/department.svg 10 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/static/images/user/droneLogo.svg 11 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/static/images/user/info.svg 21 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/static/images/user/password.svg 26 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/static/images/user/rightBtn.svg 9 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/static/images/user/userbg.svg 38 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/subPackages/deviceRegistration/add.vue 106 ●●●● patch | view | raw | blame | history
uniapps/work-wx/src/subPackages/flightApplication/index.vue 118 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/subPackages/regulationsDetail/details.vue 2 ●●● patch | view | raw | blame | history
uniapps/work-wx/src/subPackages/userDetail/infos/index.vue 5 ●●●●● patch | view | raw | blame | history
uniapps/work-wx/src/subPackages/userDetail/password/index.vue 7 ●●●●● patch | view | raw | blame | history
applications/drone-command/package.json
@@ -11,11 +11,6 @@
  },
  "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:",
@@ -23,6 +18,11 @@
    "@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:",
@@ -33,6 +33,7 @@
    "decimal.js": "catalog:",
    "disable-devtool": "catalog:",
    "echarts": "catalog:",
    "echarts-gl": "^2.0.9",
    "element-plus": "catalog:",
    "eventemitter3": "catalog:",
    "highlight.js": "catalog:",
applications/drone-command/src/assets/images/basicManage/chartBg.svg
New file
@@ -0,0 +1,132 @@
<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>
applications/drone-command/src/assets/images/basicManage/chartBg2.svg
New file
@@ -0,0 +1,132 @@
<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>
applications/drone-command/src/views/basicManage/deviceStock/DeviceChart1.vue
New file
@@ -0,0 +1,263 @@
<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>
applications/drone-command/src/views/basicManage/deviceStock/DeviceChart2.vue
New file
@@ -0,0 +1,156 @@
<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>
applications/drone-command/src/views/basicManage/deviceStock/DeviceChart3.vue
New file
@@ -0,0 +1,145 @@
<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>
applications/drone-command/src/views/basicManage/deviceStock/fwDevice.js
@@ -53,3 +53,28 @@
        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,
    })
}
applications/drone-command/src/views/basicManage/deviceStock/index.vue
@@ -1,5 +1,10 @@
<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
@@ -138,6 +143,9 @@
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: '', // 设备名称
@@ -267,4 +275,40 @@
    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>
applications/mobile-web-view/src/appPages/voiceCallDetail/index.vue
@@ -1,55 +1,55 @@
<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>
@@ -60,25 +60,69 @@
/** ✅ 你的 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
@@ -115,130 +159,130 @@
    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' }],
    })
@@ -318,6 +362,8 @@
/* ---------------- 信令处理 ---------------- */
async function onSignal(msg) {
console.log('1cesss',msg);
    if (!msg) return
    const t = (msg.type || '').toString()
@@ -343,11 +389,11 @@
        state.value = 'ringing'
        log('📞 收到来电 call,来自', incomingFrom)
        // 开始来电提示(铃声和震动)
        playRingtone()
        startVibration()
        return
    }
@@ -363,104 +409,107 @@
        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
@@ -476,12 +525,12 @@
    // 如果 offer 已经提前到达(我们暂存了),立刻 answer
    if (lastOfferSdp) {
      answerOffer(lastOfferSdp, incomingFrom)
      lastOfferSdp = null
        answerOffer(lastOfferSdp, incomingFrom)
        lastOfferSdp = null
    }
  }
  function rejectCall() {
}
function rejectCall() {
    if (!incomingFrom) return
    // 停止来电提示
@@ -491,361 +540,387 @@
    // 你后端没有 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>
pnpm-lock.yaml
@@ -281,6 +281,9 @@
      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))
@@ -4549,6 +4552,9 @@
    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'}
@@ -5098,6 +5104,11 @@
  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==}
@@ -14382,6 +14393,8 @@
      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
@@ -14887,6 +14900,12 @@
  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
uniapps/work-app/src/hooks/useGlobalWS.js
@@ -1,104 +1,167 @@
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 }
  )
}
uniapps/work-app/src/pages.json
@@ -79,13 +79,15 @@
        {
          "path": "userDetail/infos/index",
          "style": {
            "navigationBarTitleText": "个人资料"
            "navigationBarTitleText": "个人资料",
            "navigationStyle": "custom"
          }
        },
        {
          "path": "userDetail/password/index",
          "style": {
            "navigationBarTitleText": "修改密码"
            "navigationBarTitleText": "修改密码",
            "navigationStyle": "custom"
          }
        },
        {
uniapps/work-app/src/pages/login/index.vue
@@ -45,6 +45,7 @@
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";
@@ -134,7 +135,59 @@
      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",
    });
uniapps/work-app/src/subPackages/userDetail/infos/index.vue
@@ -1,5 +1,14 @@
<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" -->
@@ -219,20 +228,34 @@
        });
    };
// 返回按钮点击事件
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;
@@ -256,7 +279,7 @@
        background: #FFFFFF;
        border-radius: 12rpx;
        box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
    margin-top: 88rpx;
        .detailCon {
            padding: 0 24rpx;
        }
uniapps/work-app/src/subPackages/userDetail/password/index.vue
@@ -1,6 +1,15 @@
<!-- 修改密码 -->
<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">
@@ -213,7 +222,7 @@
      password2: password2,
            code
        };
        resetPassword(resetData).then(res => {
            if (res.data.code === 200) {
                uni.showToast({
@@ -242,21 +251,35 @@
            });
        });
    };
  // 返回按钮点击事件
  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;
@@ -277,7 +300,7 @@
        background: #FFFFFF;
        border-radius: 12rpx;
        box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
    margin-top: 88rpx;
        .detailCon {
            padding: 0 24rpx;
        }
uniapps/work-app/src/subPackages/voiceCallDetail/index.vue
@@ -21,7 +21,7 @@
  // 构建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
uniapps/work-app/src/utils/common/index.js
@@ -90,4 +90,4 @@
export function getAssetsImage (targetUrl) {
  const url = getEnvObj().VITE_APP_ASSETS_URL
  return `${url}${targetUrl}`
}
}
uniapps/work-app/src/utils/websocket.js
New file
@@ -0,0 +1,188 @@
// 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()
uniapps/work-wx/src/api/index.js
@@ -106,4 +106,4 @@
    url: `/system/sys/SysRegionCode/allTree`,
    method: 'get'
  })
}
}
uniapps/work-wx/src/api/user/index.js
@@ -137,3 +137,11 @@
    data: data,
  });
};
// 验证码
export const registerSendCodeApi = (phone) => {
  return request({
    url: `/system/verificationCode/registerSendCode/${phone}`,
    method: 'get'
  })
}
uniapps/work-wx/src/pages/equipmentManagement/index.vue
@@ -1,19 +1,21 @@
<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>
@@ -29,17 +31,31 @@
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)
  }
@@ -61,6 +77,14 @@
    url: '/subPackages/deviceRegistration/add',
  })
}
function scrolltolower() {
  formParams.value.current++
  // 停止不刷新
  if (equipmentList.value.length < formParams.value.size) {
    return
  }
  getEquipmentList()
}
onMounted(() => {
  getEquipmentList()
})
uniapps/work-wx/src/pages/legalPolicy/index.vue
@@ -1,21 +1,23 @@
<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>
@@ -36,30 +38,51 @@
  },
])
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,
@@ -67,6 +90,12 @@
      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,
@@ -81,8 +110,6 @@
    url: '/subPackages/regulationsDetail/details?id=' + item.noticeId+'&type=2',
  })
}
onMounted(() => {
  getSysNoticeList()
uniapps/work-wx/src/pages/login/index.vue
@@ -6,10 +6,10 @@
    <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" /> -->
uniapps/work-wx/src/pages/register/index.vue
@@ -10,7 +10,7 @@
             </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>
@@ -44,7 +44,7 @@
</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: '',
@@ -115,8 +115,7 @@
    }
    // 这里假设账号就是手机号,实际项目中可能需要验证格式
    sendResetCode(formParams.value.userName).then(res => {
        console.log('4444',res)
  registerSendCodeApi(formParams.value.userName).then(res => {
        if (res.data.code === 200) {
            uni.showToast({
                title: '验证码发送成功',
@@ -190,6 +189,10 @@
                title: '注册成功',
                icon: 'success',
            });
            // 跳转登录页面
            uni.navigateTo({
                url: '/pages/login/index'
            })
        } else {
            uni.showToast({
                title: res.data.msg || '注册失败',
@@ -214,7 +217,7 @@
        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 {
@@ -238,6 +241,9 @@
            font-weight: 400;
            font-size: 15px;
            color: #222324;
            .nike {
                margin-right: 16rpx;
            }
            .required {
                color: #FF4444;
            }
uniapps/work-wx/src/pages/user/index.vue
@@ -1,48 +1,45 @@
<!-- 我的 -->
<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>
@@ -54,7 +51,7 @@
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();
//
@@ -100,7 +97,7 @@
  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 {
@@ -213,13 +210,13 @@
  }
  .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%;
  }
uniapps/work-wx/src/static/images/user/default-header.png

uniapps/work-wx/src/static/images/user/default-header.svg
File was deleted
uniapps/work-wx/src/static/images/user/department.svg
File was deleted
uniapps/work-wx/src/static/images/user/droneLogo.svg
File was deleted
uniapps/work-wx/src/static/images/user/info.svg
File was deleted
uniapps/work-wx/src/static/images/user/password.svg
File was deleted
uniapps/work-wx/src/static/images/user/rightBtn.svg
File was deleted
uniapps/work-wx/src/static/images/user/userbg.svg
File was deleted
uniapps/work-wx/src/subPackages/deviceRegistration/add.vue
@@ -144,19 +144,21 @@
        <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="民用航空器登记证书"
@@ -259,9 +261,16 @@
        />
        <!-- 所属区域 -->
        <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>
@@ -293,6 +302,8 @@
    hasInsurance: '', // 是否有保险
    hasInsuranceText: '', // 是否有保险文本
    region: '', // 所属区域
    regionCode: '', // 所属区域
    regionText: '', // 所属区域文本
    caacRegistrationCode: '', // 民用航空器登记证书
    caacRegistrationCodeText: '', // 民用航空器登记证书文本
    deviceImage: '', // 飞行器照片
@@ -341,6 +352,38 @@
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([
@@ -455,13 +498,13 @@
// 获取所属单位
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) {
    // 获取上传的文件对象
@@ -543,8 +586,29 @@
        // 这里可以添加额外的失败处理逻辑
    }
}
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">
uniapps/work-wx/src/subPackages/flightApplication/index.vue
@@ -12,23 +12,25 @@
            </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>
@@ -54,65 +56,23 @@
}
// 审批记录是很多条
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,
@@ -120,6 +80,22 @@
            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()
@@ -129,9 +105,9 @@
.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 {
@@ -175,7 +151,7 @@
        width: 90%;
        left: 50%;
        transform: translateX(-50%);
        top: 340rpx !important;
        top: 350rpx !important;
        height: 68rpx;
        flex: 0;
    }
uniapps/work-wx/src/subPackages/regulationsDetail/details.vue
@@ -177,7 +177,7 @@
        }
    }
    .fj-list {
        margin-top: 40rpx;
        // margin-top: 20rpx;
    }
    .fj-item {
        display: flex;
uniapps/work-wx/src/subPackages/userDetail/infos/index.vue
@@ -252,8 +252,9 @@
        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;
uniapps/work-wx/src/subPackages/userDetail/password/index.vue
@@ -34,7 +34,7 @@
</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";
@@ -260,8 +260,8 @@
            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 {
@@ -293,7 +293,6 @@
                    font-weight: 400;
                    font-size: 30rpx;
                    color: #222324;
                    white-space: nowrap;
                }
                input.input-item {