1
0

6 کامیت‌ها 77853314b5 ... 3a58b72b84

نویسنده SHA1 پیام تاریخ
  qxj 3a58b72b84 add 1 ماه پیش
  qxj c2c2c5e3b5 add 1 ماه پیش
  qxj 7c596e1e4e add 1 ماه پیش
  qxj 74e7aeacf7 add 2 ماه پیش
  qxj 0e13d51329 Merge branch 'master' of http://1.14.104.71:10880/liujiaxin/zhibo 3 ماه پیش
  qxj 184575e83b add 3 ماه پیش
8فایلهای تغییر یافته به همراه559 افزوده شده و 935 حذف شده
  1. 6 565
      package-lock.json
  2. 2 1
      package.json
  3. 297 266
      pages_course/components/viewer.vue
  4. BIN
      pages_course/living 2.vue.zip
  5. 136 102
      pages_course/living.vue
  6. BIN
      pages_course/living.vue.zip
  7. 2 1
      pages_course/livingList.vue
  8. 116 0
      ws-load-test.js

+ 6 - 565
package-lock.json

@@ -1,509 +1,14 @@
 {
     "name": "shop",
     "version": "1.0.0",
-    "lockfileVersion": 2,
+    "lockfileVersion": 1,
     "requires": true,
-    "packages": {
-        "": {
-            "name": "shop",
-            "version": "1.0.0",
-            "license": "ISC",
-            "dependencies": {
-                "animate.css": "^3.7.2",
-                "cos-wx-sdk-v5": "^1.0.10",
-                "crypto": "^1.0.1",
-                "crypto-js": "^4.2.0",
-                "dayjs": "^1.11.18",
-                "hls": "0.0.1",
-                "tim-wx-sdk": "^2.17.0",
-                "vuex": "^4.1.0"
-            },
-            "devDependencies": {}
-        },
-        "node_modules/@babel/helper-string-parser": {
-            "version": "7.27.1",
-            "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
-            "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
-            "peer": true,
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/@babel/helper-validator-identifier": {
-            "version": "7.28.5",
-            "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
-            "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
-            "peer": true,
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/@babel/parser": {
-            "version": "7.28.5",
-            "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
-            "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
-            "peer": true,
-            "dependencies": {
-                "@babel/types": "^7.28.5"
-            },
-            "bin": {
-                "parser": "bin/babel-parser.js"
-            },
-            "engines": {
-                "node": ">=6.0.0"
-            }
-        },
-        "node_modules/@babel/types": {
-            "version": "7.28.5",
-            "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
-            "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
-            "peer": true,
-            "dependencies": {
-                "@babel/helper-string-parser": "^7.27.1",
-                "@babel/helper-validator-identifier": "^7.28.5"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/@jridgewell/sourcemap-codec": {
-            "version": "1.5.5",
-            "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
-            "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
-            "peer": true
-        },
-        "node_modules/@vue/compiler-core": {
-            "version": "3.5.22",
-            "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.22.tgz",
-            "integrity": "sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==",
-            "peer": true,
-            "dependencies": {
-                "@babel/parser": "^7.28.4",
-                "@vue/shared": "3.5.22",
-                "entities": "^4.5.0",
-                "estree-walker": "^2.0.2",
-                "source-map-js": "^1.2.1"
-            }
-        },
-        "node_modules/@vue/compiler-dom": {
-            "version": "3.5.22",
-            "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.22.tgz",
-            "integrity": "sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==",
-            "peer": true,
-            "dependencies": {
-                "@vue/compiler-core": "3.5.22",
-                "@vue/shared": "3.5.22"
-            }
-        },
-        "node_modules/@vue/compiler-sfc": {
-            "version": "3.5.22",
-            "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz",
-            "integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==",
-            "peer": true,
-            "dependencies": {
-                "@babel/parser": "^7.28.4",
-                "@vue/compiler-core": "3.5.22",
-                "@vue/compiler-dom": "3.5.22",
-                "@vue/compiler-ssr": "3.5.22",
-                "@vue/shared": "3.5.22",
-                "estree-walker": "^2.0.2",
-                "magic-string": "^0.30.19",
-                "postcss": "^8.5.6",
-                "source-map-js": "^1.2.1"
-            }
-        },
-        "node_modules/@vue/compiler-ssr": {
-            "version": "3.5.22",
-            "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.22.tgz",
-            "integrity": "sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==",
-            "peer": true,
-            "dependencies": {
-                "@vue/compiler-dom": "3.5.22",
-                "@vue/shared": "3.5.22"
-            }
-        },
-        "node_modules/@vue/devtools-api": {
-            "version": "6.6.4",
-            "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
-            "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="
-        },
-        "node_modules/@vue/reactivity": {
-            "version": "3.5.22",
-            "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.22.tgz",
-            "integrity": "sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==",
-            "peer": true,
-            "dependencies": {
-                "@vue/shared": "3.5.22"
-            }
-        },
-        "node_modules/@vue/runtime-core": {
-            "version": "3.5.22",
-            "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.22.tgz",
-            "integrity": "sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==",
-            "peer": true,
-            "dependencies": {
-                "@vue/reactivity": "3.5.22",
-                "@vue/shared": "3.5.22"
-            }
-        },
-        "node_modules/@vue/runtime-dom": {
-            "version": "3.5.22",
-            "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.22.tgz",
-            "integrity": "sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==",
-            "peer": true,
-            "dependencies": {
-                "@vue/reactivity": "3.5.22",
-                "@vue/runtime-core": "3.5.22",
-                "@vue/shared": "3.5.22",
-                "csstype": "^3.1.3"
-            }
-        },
-        "node_modules/@vue/server-renderer": {
-            "version": "3.5.22",
-            "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.22.tgz",
-            "integrity": "sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==",
-            "peer": true,
-            "dependencies": {
-                "@vue/compiler-ssr": "3.5.22",
-                "@vue/shared": "3.5.22"
-            },
-            "peerDependencies": {
-                "vue": "3.5.22"
-            }
-        },
-        "node_modules/@vue/shared": {
-            "version": "3.5.22",
-            "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.22.tgz",
-            "integrity": "sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==",
-            "peer": true
-        },
-        "node_modules/animate.css": {
-            "version": "3.7.2",
-            "resolved": "https://registry.npmjs.org/animate.css/-/animate.css-3.7.2.tgz",
-            "integrity": "sha512-0bE8zYo7C0KvgOYrSVfrzkbYk6IOTVPNqkiHg2cbyF4Pq/PXzilz4BRWA3hwEUBoMp5VBgrC29lQIZyhRWdBTw=="
-        },
-        "node_modules/cos-wx-sdk-v5": {
-            "version": "1.1.5",
-            "resolved": "https://registry.npmjs.org/cos-wx-sdk-v5/-/cos-wx-sdk-v5-1.1.5.tgz",
-            "integrity": "sha512-++O7HD6Hz6UDlhgKMchJOap85bQtY+DKzPg2r5uCdyRb45AWC+Xj9qetXohPpA2G/inNVSqxw/EtjGPe2OIhyg==",
-            "dependencies": {
-                "mime": "^2.4.6",
-                "xmldom": "^0.1.31"
-            }
-        },
-        "node_modules/crypto": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
-            "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
-            "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in."
-        },
-        "node_modules/crypto-js": {
-            "version": "4.2.0",
-            "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
-            "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
-        },
-        "node_modules/csstype": {
-            "version": "3.1.3",
-            "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
-            "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
-            "peer": true
-        },
-        "node_modules/dayjs": {
-            "version": "1.11.18",
-            "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz",
-            "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA=="
-        },
-        "node_modules/entities": {
-            "version": "4.5.0",
-            "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
-            "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
-            "peer": true,
-            "engines": {
-                "node": ">=0.12"
-            },
-            "funding": {
-                "url": "https://github.com/fb55/entities?sponsor=1"
-            }
-        },
-        "node_modules/estree-walker": {
-            "version": "2.0.2",
-            "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
-            "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
-            "peer": true
-        },
-        "node_modules/hls": {
-            "version": "0.0.1",
-            "resolved": "https://registry.npmjs.org/hls/-/hls-0.0.1.tgz",
-            "integrity": "sha512-ov6aIzckaDCdFFAeJbrmZYhAR0O7w+nDXh3fz9XEcf8P6EercldLht/23JHRFXmAMtvKw8dzuOCafzktdV1AZw=="
-        },
-        "node_modules/magic-string": {
-            "version": "0.30.21",
-            "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
-            "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
-            "peer": true,
-            "dependencies": {
-                "@jridgewell/sourcemap-codec": "^1.5.5"
-            }
-        },
-        "node_modules/mime": {
-            "version": "2.6.0",
-            "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
-            "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
-            "bin": {
-                "mime": "cli.js"
-            },
-            "engines": {
-                "node": ">=4.0.0"
-            }
-        },
-        "node_modules/nanoid": {
-            "version": "3.3.11",
-            "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
-            "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
-            "funding": [
-                {
-                    "type": "github",
-                    "url": "https://github.com/sponsors/ai"
-                }
-            ],
-            "peer": true,
-            "bin": {
-                "nanoid": "bin/nanoid.cjs"
-            },
-            "engines": {
-                "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
-            }
-        },
-        "node_modules/picocolors": {
-            "version": "1.1.1",
-            "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
-            "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
-            "peer": true
-        },
-        "node_modules/postcss": {
-            "version": "8.5.6",
-            "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
-            "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
-            "funding": [
-                {
-                    "type": "opencollective",
-                    "url": "https://opencollective.com/postcss/"
-                },
-                {
-                    "type": "tidelift",
-                    "url": "https://tidelift.com/funding/github/npm/postcss"
-                },
-                {
-                    "type": "github",
-                    "url": "https://github.com/sponsors/ai"
-                }
-            ],
-            "peer": true,
-            "dependencies": {
-                "nanoid": "^3.3.11",
-                "picocolors": "^1.1.1",
-                "source-map-js": "^1.2.1"
-            },
-            "engines": {
-                "node": "^10 || ^12 || >=14"
-            }
-        },
-        "node_modules/source-map-js": {
-            "version": "1.2.1",
-            "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
-            "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
-            "peer": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/tim-wx-sdk": {
-            "version": "2.18.0",
-            "resolved": "https://registry.npmjs.org/tim-wx-sdk/-/tim-wx-sdk-2.18.0.tgz",
-            "integrity": "sha512-Dz6aHpaCdk1ST/ZzltliSFHBsB5CdFU+q2NpFZc9PV8br0a5F2GyYgrdLn1Yqt8YntEwMReaud3LUY638zNJug=="
-        },
-        "node_modules/vue": {
-            "version": "3.5.22",
-            "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
-            "integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
-            "peer": true,
-            "dependencies": {
-                "@vue/compiler-dom": "3.5.22",
-                "@vue/compiler-sfc": "3.5.22",
-                "@vue/runtime-dom": "3.5.22",
-                "@vue/server-renderer": "3.5.22",
-                "@vue/shared": "3.5.22"
-            },
-            "peerDependencies": {
-                "typescript": "*"
-            },
-            "peerDependenciesMeta": {
-                "typescript": {
-                    "optional": true
-                }
-            }
-        },
-        "node_modules/vuex": {
-            "version": "4.1.0",
-            "resolved": "https://registry.npmjs.org/vuex/-/vuex-4.1.0.tgz",
-            "integrity": "sha512-hmV6UerDrPcgbSy9ORAtNXDr9M4wlNP4pEFKye4ujJF8oqgFFuxDCdOLS3eNoRTtq5O3hoBDh9Doj1bQMYHRbQ==",
-            "dependencies": {
-                "@vue/devtools-api": "^6.0.0-beta.11"
-            },
-            "peerDependencies": {
-                "vue": "^3.2.0"
-            }
-        },
-        "node_modules/xmldom": {
-            "version": "0.1.31",
-            "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz",
-            "integrity": "sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==",
-            "deprecated": "Deprecated due to CVE-2021-21366 resolved in 0.5.0",
-            "engines": {
-                "node": ">=0.1"
-            }
-        }
-    },
     "dependencies": {
-        "@babel/helper-string-parser": {
-            "version": "7.27.1",
-            "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
-            "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
-            "peer": true
-        },
-        "@babel/helper-validator-identifier": {
-            "version": "7.28.5",
-            "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
-            "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
-            "peer": true
-        },
-        "@babel/parser": {
-            "version": "7.28.5",
-            "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
-            "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
-            "peer": true,
-            "requires": {
-                "@babel/types": "^7.28.5"
-            }
-        },
-        "@babel/types": {
-            "version": "7.28.5",
-            "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
-            "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
-            "peer": true,
-            "requires": {
-                "@babel/helper-string-parser": "^7.27.1",
-                "@babel/helper-validator-identifier": "^7.28.5"
-            }
-        },
-        "@jridgewell/sourcemap-codec": {
-            "version": "1.5.5",
-            "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
-            "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
-            "peer": true
-        },
-        "@vue/compiler-core": {
-            "version": "3.5.22",
-            "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.22.tgz",
-            "integrity": "sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==",
-            "peer": true,
-            "requires": {
-                "@babel/parser": "^7.28.4",
-                "@vue/shared": "3.5.22",
-                "entities": "^4.5.0",
-                "estree-walker": "^2.0.2",
-                "source-map-js": "^1.2.1"
-            }
-        },
-        "@vue/compiler-dom": {
-            "version": "3.5.22",
-            "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.22.tgz",
-            "integrity": "sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==",
-            "peer": true,
-            "requires": {
-                "@vue/compiler-core": "3.5.22",
-                "@vue/shared": "3.5.22"
-            }
-        },
-        "@vue/compiler-sfc": {
-            "version": "3.5.22",
-            "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz",
-            "integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==",
-            "peer": true,
-            "requires": {
-                "@babel/parser": "^7.28.4",
-                "@vue/compiler-core": "3.5.22",
-                "@vue/compiler-dom": "3.5.22",
-                "@vue/compiler-ssr": "3.5.22",
-                "@vue/shared": "3.5.22",
-                "estree-walker": "^2.0.2",
-                "magic-string": "^0.30.19",
-                "postcss": "^8.5.6",
-                "source-map-js": "^1.2.1"
-            }
-        },
-        "@vue/compiler-ssr": {
-            "version": "3.5.22",
-            "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.22.tgz",
-            "integrity": "sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==",
-            "peer": true,
-            "requires": {
-                "@vue/compiler-dom": "3.5.22",
-                "@vue/shared": "3.5.22"
-            }
-        },
         "@vue/devtools-api": {
             "version": "6.6.4",
             "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
             "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="
         },
-        "@vue/reactivity": {
-            "version": "3.5.22",
-            "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.22.tgz",
-            "integrity": "sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==",
-            "peer": true,
-            "requires": {
-                "@vue/shared": "3.5.22"
-            }
-        },
-        "@vue/runtime-core": {
-            "version": "3.5.22",
-            "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.22.tgz",
-            "integrity": "sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==",
-            "peer": true,
-            "requires": {
-                "@vue/reactivity": "3.5.22",
-                "@vue/shared": "3.5.22"
-            }
-        },
-        "@vue/runtime-dom": {
-            "version": "3.5.22",
-            "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.22.tgz",
-            "integrity": "sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==",
-            "peer": true,
-            "requires": {
-                "@vue/reactivity": "3.5.22",
-                "@vue/runtime-core": "3.5.22",
-                "@vue/shared": "3.5.22",
-                "csstype": "^3.1.3"
-            }
-        },
-        "@vue/server-renderer": {
-            "version": "3.5.22",
-            "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.22.tgz",
-            "integrity": "sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==",
-            "peer": true,
-            "requires": {
-                "@vue/compiler-ssr": "3.5.22",
-                "@vue/shared": "3.5.22"
-            }
-        },
-        "@vue/shared": {
-            "version": "3.5.22",
-            "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.22.tgz",
-            "integrity": "sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==",
-            "peer": true
-        },
         "animate.css": {
             "version": "3.7.2",
             "resolved": "https://registry.npmjs.org/animate.css/-/animate.css-3.7.2.tgz",
@@ -528,95 +33,26 @@
             "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
             "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
         },
-        "csstype": {
-            "version": "3.1.3",
-            "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
-            "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
-            "peer": true
-        },
         "dayjs": {
             "version": "1.11.18",
             "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz",
             "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA=="
         },
-        "entities": {
-            "version": "4.5.0",
-            "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
-            "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
-            "peer": true
-        },
-        "estree-walker": {
-            "version": "2.0.2",
-            "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
-            "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
-            "peer": true
-        },
         "hls": {
             "version": "0.0.1",
             "resolved": "https://registry.npmjs.org/hls/-/hls-0.0.1.tgz",
             "integrity": "sha512-ov6aIzckaDCdFFAeJbrmZYhAR0O7w+nDXh3fz9XEcf8P6EercldLht/23JHRFXmAMtvKw8dzuOCafzktdV1AZw=="
         },
-        "magic-string": {
-            "version": "0.30.21",
-            "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
-            "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
-            "peer": true,
-            "requires": {
-                "@jridgewell/sourcemap-codec": "^1.5.5"
-            }
-        },
         "mime": {
             "version": "2.6.0",
             "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
             "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="
         },
-        "nanoid": {
-            "version": "3.3.11",
-            "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
-            "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
-            "peer": true
-        },
-        "picocolors": {
-            "version": "1.1.1",
-            "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
-            "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
-            "peer": true
-        },
-        "postcss": {
-            "version": "8.5.6",
-            "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
-            "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
-            "peer": true,
-            "requires": {
-                "nanoid": "^3.3.11",
-                "picocolors": "^1.1.1",
-                "source-map-js": "^1.2.1"
-            }
-        },
-        "source-map-js": {
-            "version": "1.2.1",
-            "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
-            "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
-            "peer": true
-        },
         "tim-wx-sdk": {
             "version": "2.18.0",
             "resolved": "https://registry.npmjs.org/tim-wx-sdk/-/tim-wx-sdk-2.18.0.tgz",
             "integrity": "sha512-Dz6aHpaCdk1ST/ZzltliSFHBsB5CdFU+q2NpFZc9PV8br0a5F2GyYgrdLn1Yqt8YntEwMReaud3LUY638zNJug=="
         },
-        "vue": {
-            "version": "3.5.22",
-            "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
-            "integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
-            "peer": true,
-            "requires": {
-                "@vue/compiler-dom": "3.5.22",
-                "@vue/compiler-sfc": "3.5.22",
-                "@vue/runtime-dom": "3.5.22",
-                "@vue/server-renderer": "3.5.22",
-                "@vue/shared": "3.5.22"
-            }
-        },
         "vuex": {
             "version": "4.1.0",
             "resolved": "https://registry.npmjs.org/vuex/-/vuex-4.1.0.tgz",
@@ -625,6 +61,11 @@
                 "@vue/devtools-api": "^6.0.0-beta.11"
             }
         },
+        "ws": {
+            "version": "8.19.0",
+            "resolved": "https://registry.npmmirror.com/ws/-/ws-8.19.0.tgz",
+            "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="
+        },
         "xmldom": {
             "version": "0.1.31",
             "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz",

+ 2 - 1
package.json

@@ -11,7 +11,8 @@
         "dayjs": "^1.11.18",
         "hls": "0.0.1",
         "tim-wx-sdk": "^2.17.0",
-        "vuex": "^4.1.0"
+        "vuex": "^4.1.0",
+        "ws": "^8.19.0"
     },
     "main": "main.js",
     "scripts": {

+ 297 - 266
pages_course/components/viewer.vue

@@ -1,277 +1,308 @@
 <template>
-  <u-popup 
-    :show="show" 
-    @close="handleClose" 
-    @open="handleOpen"
-    round="20rpx" 
-    bgColor="#ffffff" 
-    zIndex="10077"
-  >
-    <view class="viewer-list-popup">
-      <view class="popup-header fs32">在线观众</view>
-      
-      <scroll-view 
-        v-if="Array.isArray(viewers)" 
-        scroll-y 
-        class="scroll-content"
-        :style="{ height: scrollHeight + 'px' }" 
-        @scrolltolower="handleScrollToLower"
-      >
-        <view 
-          class="viewer-item x-f mb20 mt20" 
-          v-for="(item, index) in viewers" 
-          :key="getViewerKey(item, index)"
-        >
-         <!-- <view 
-            class="rank-number"
-            :style="getRankStyle(index)"
-          >
-            {{ index + 1 }}
-          </view> -->
-		  <view 
-		    class="rank-number"
-		    :style="{
-		      color: index < 3 ? ['#FF3B30', '#FF9500', '#FFCC00'][index] : '#8E8E93',
-		      fontWeight: index < 3 ? 'bold' : 'normal',
-		      width: '50rpx'
-		    }"
-		  >
-		    {{ index + 1 }}
-		  </view>
-          
-          <!-- 用户头像 -->
-          <view class="avatar-container">
-            <u-avatar 
-              v-if="item.avatar" 
-              :src="item.avatar" 
-              :size="36"
-            ></u-avatar>
-            <view 
-              v-else 
-              class="default-avatar"
-              :style="{ backgroundColor: getUserRandomColor(item.userId) }"
-            >
-              <text class="avatar-text">{{ getNicknameInitial(item.nickName) }}</text>
-            </view>
-          </view>
-          
-          <text class="nickname ml16 f30">{{ item.nickName || '未命名' }}</text>
-        </view>
-        
-        <!-- 加载状态 -->
-        <view v-if="loading" class="loading-text">
-          <u-loading-icon size="16"></u-loading-icon>
-          加载中...
-        </view>
-        
-        <!-- 无数据提示 -->
-        <view v-if="!loading && viewers.length === 0" class="empty-text">
-          暂无在线观众
-        </view>
-      </scroll-view>
-    </view>
-  </u-popup>
+	<u-popup :show="show" mode="bottom" @close="handleClose" @open="handleOpen" round="20rpx" bgColor="#ffffff" zIndex="10077">
+		<view class="viewer-list-popup">
+			<view class="popup-header fs32">在线观众</view>
+
+			<view class="list-container">
+				<mescroll-uni ref="mescrollRef" :fixed="false" :down="downOption" :up="upOption" @init="mescrollInit" @down="downCallback" @up="upCallback">
+					<view class="viewer-item x-f mb20 mt20" v-for="(item, index) in viewers" :key="getViewerKey(item, index)">
+						<!-- <view 
+				class="rank-number"
+				:style="getRankStyle(index)"
+			>
+				{{ index + 1 }}
+			</view> -->
+						<view
+							class="rank-number"
+							:style="{
+								color: index < 3 ? ['#FF3B30', '#FF9500', '#FFCC00'][index] : '#8E8E93',
+								fontWeight: index < 3 ? 'bold' : 'normal',
+								width: '50rpx'
+							}"
+						>
+							{{ index + 1 }}
+						</view>
+
+						<!-- 用户头像 -->
+						<view class="avatar-container">
+							<u-avatar v-if="item.avatar" :src="item.avatar" :size="36"></u-avatar>
+							<view v-else class="default-avatar" :style="{ backgroundColor: getUserRandomColor(item.userId) }">
+								<text class="avatar-text">{{ getNicknameInitial(item.nickName) }}</text>
+							</view>
+						</view>
+
+						<text class="nickname ml16 f30">{{ item.nickName || '未命名' }}</text>
+					</view>
+
+					<!-- mescroll 自带 loading 和 noMore,不需要这里的 loading-text -->
+					
+					<!-- 无数据提示 -->
+					<!-- <view v-if="!loading && viewers.length === 0" class="empty-text">暂无在线观众</view> -->
+					
+					<view v-if="lookAudsCount > 0" class="more-text">还有{{lookAudsCount}}用户正在观看</view>
+					
+				</mescroll-uni>
+			</view>
+		</view>
+	</u-popup>
 </template>
 
 <script>
+import MescrollUni from "@/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-uni.vue";
 export default {
-  // name: 'Viewer',
-  
-  props: {
-    // 是否显示弹窗
-    show: {
-      type: Boolean,
-      default: false
-    },
-    // 观众列表数据
-    viewers: {
-      type: Array,
-      default: () => []
-    },
-    // 是否正在加载
-    loading: {
-      type: Boolean,
-      default: false
-    },
-    // 滚动区域高度
-    scrollHeight: {
-      type: Number,
-      default: 400
-    }
-  },
-  
-  data() {
-    return {
-      // 本地状态可以根据需要添加
-    };
-  },
-  
-  methods: {
-    /**
-     * 处理关闭事件
-     */
-    handleClose() {
-      this.$emit('close');
-    },
-    
-    /**
-     * 处理打开事件
-     */
-    handleOpen() {
-      this.$emit('open');
-    },
-    
-    /**
-     * 处理滚动到底部事件
-     */
-    handleScrollToLower() {
-      this.$emit('scrolltolower');
-    },
-    
-    /**
-     * 生成观众项的唯一key
-     */
-    getViewerKey(item, index) {
-      return item.userId ? `viewer_${item.userId}` : `viewer_${index}`;
-    },
-    
-    /**
-     * 获取排名样式
-     */
-    getRankStyle(index) {
-      const rankColors = {
-        0: '#FF3B30', // 第一名
-        1: '#FF9500', // 第二名  
-        2: '#FFCC00'  // 第三名
-      };
-      
-      return {
-        color: rankColors[index] || '#8E8E93',
-        fontWeight: index < 3 ? 'bold' : 'normal',
-        width: '50rpx'
-      };
-    },
-    
-    /**
-     * 获取用户随机颜色(从父组件传入或本地实现)
-     */
-    getUserRandomColor(userId) {
-      // 如果父组件传入了方法,可以使用父组件的方法
-      // 否则这里实现一个简单的版本
-      if (this.$parent && this.$parent.getUserRandomColor) {
-        return this.$parent.getUserRandomColor(userId);
-      }
-      
-      // 简单的本地实现
-      if (!userId) return '#8978e2';
-      
-      const colorPool = [
-        '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7',
-        '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9'
-      ];
-      
-      let seed = 0;
-      for (let i = 0; i < userId.length; i++) {
-        seed = (seed * 31 + userId.charCodeAt(i)) % 1000000;
-      }
-      return colorPool[seed % colorPool.length];
-    },
-    
-    /**
-     * 获取昵称首字母
-     */
-    getNicknameInitial(nickName) {
-      if (!nickName || typeof nickName !== 'string') return '未';
-      if (/^[\u4e00-\u9fa5]/.test(nickName[0])) {
-        return nickName[0];
-      }
-      return nickName[0].toUpperCase();
-    }
-  },
-  
-  watch: {
-    // 监听显示状态变化
-    show(newVal) {
-      if (newVal) {
-        this.$emit('open');
-      }
-    }
-  }
+	// name: 'Viewer',
+	components: {
+		MescrollUni
+	},
+	props: {
+		// 是否显示弹窗
+		show: {
+			type: Boolean,
+			default: false
+		},
+		// 观众列表数据
+		viewers: {
+			type: Array,
+			default: () => []
+		},
+		// 是否正在加载
+		loading: {
+			type: Boolean,
+			default: false
+		},
+		// 滚动区域高度
+		scrollHeight: {
+			type: Number,
+			default: 400
+		},
+		lookAudsCount: {
+			type: Number,
+			default: 0
+		}
+		
+		
+	},
+
+	data() {
+		return {
+			mescroll: null,
+			downOption: {
+				use: false // 禁用下拉刷新
+			},
+			upOption: {
+				auto: false, // 不自动加载,由父组件控制或者 open 时触发
+				noMoreSize: 5,
+				textNoMore: '没有更多了',
+				empty: {
+					use: true,
+					icon: "/static/images/no_data.png", // 假设有
+					tip: "暂无在线观众"
+				},
+				toTop: {
+					src: '' // 不显示回到顶部按钮
+				}
+			}
+		};
+	},
+
+	methods: {
+		mescrollInit(mescroll) {
+			this.mescroll = mescroll;
+		},
+		downCallback() {
+			this.mescroll.endSuccess();
+		},
+		upCallback(page) {
+			this.$emit('loadMore', page);
+		},
+		// 暴露给父组件的方法
+		endSuccess(curPageLen, hasNext) {
+			if (this.mescroll) {
+				this.mescroll.endSuccess(curPageLen, hasNext);
+			}
+		},
+		endErr() {
+			if (this.mescroll) {
+				this.mescroll.endErr();
+			}
+		},
+		reset() {
+			if (this.mescroll) {
+				this.mescroll.resetUpScroll();
+			}
+		},
+		/**
+		 * 处理关闭事件
+		 */
+		handleClose() {
+			this.$emit('close');
+		},
+
+		/**
+		 * 处理打开事件
+		 */
+		handleOpen() {
+			this.$emit('open');
+		},
+
+		/**
+		 * 处理滚动到底部事件
+		 */
+		getViewerKey(item, index) {
+			return item.userId ? `viewer_${item.userId}` : `viewer_${index}`;
+		},
+
+		/**
+		 * 获取排名样式
+		 */
+		getRankStyle(index) {
+			const rankColors = {
+				0: '#FF3B30', // 第一名
+				1: '#FF9500', // 第二名
+				2: '#FFCC00' // 第三名
+			};
+
+			return {
+				color: rankColors[index] || '#8E8E93',
+				fontWeight: index < 3 ? 'bold' : 'normal',
+				width: '50rpx'
+			};
+		},
+
+		/**
+		 * 获取用户随机颜色(从父组件传入或本地实现)
+		 */
+		getUserRandomColor(userId) {
+			// 如果父组件传入了方法,可以使用父组件的方法
+			// 否则这里实现一个简单的版本
+			if (this.$parent && this.$parent.getUserRandomColor) {
+				return this.$parent.getUserRandomColor(userId);
+			}
+
+			// 简单的本地实现
+			if (!userId) return '#8978e2';
+
+			const colorPool = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9'];
+
+			let seed = 0;
+			for (let i = 0; i < userId.length; i++) {
+				seed = (seed * 31 + userId.charCodeAt(i)) % 1000000;
+			}
+			return colorPool[seed % colorPool.length];
+		},
+
+		/**
+		 * 获取昵称首字母
+		 */
+		getNicknameInitial(nickName) {
+			if (!nickName || typeof nickName !== 'string') return '未';
+			if (/^[\u4e00-\u9fa5]/.test(nickName[0])) {
+				return nickName[0];
+			}
+			return nickName[0].toUpperCase();
+		}
+	},
+
+	watch: {
+		// 监听显示状态变化
+		show(newVal) {
+			if (newVal) {
+				this.$emit('open');
+				this.$nextTick(() => {
+					this.reset();
+				});
+			}
+		}
+	}
 };
 </script>
 
 <style scoped lang="scss">
 .viewer-list-popup {
-  position: relative;
-  height: 60vh;
-  padding: 40rpx 0rpx;
-  box-sizing: border-box;
-  display: flex;
-  flex-direction: column;
-  
-  .popup-header {
-    text-align: center;
-    font-weight: 600;
-    margin-bottom: 20rpx;
-  }
-  
-  .scroll-content {
-    flex: 1;
-    overflow-y: auto;
-    padding: 0 40rpx;
-  }
-  
-  .viewer-item {
-    align-items: center;
-    padding: 16rpx 0;
-    
-    .rank-number {
-      text-align: center;
-      margin-right: 20rpx;
-      font-size: 28rpx;
-    }
-    
-    .avatar-container {
-      .default-avatar {
-        width: 72rpx;
-        height: 72rpx;
-        border-radius: 50%;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        
-        .avatar-text {
-          color: #ffffff;
-          font-size: 24rpx;
-          font-weight: 500;
-        }
-      }
-    }
-    
-    .nickname {
-      flex: 1;
-      overflow: hidden;
-      text-overflow: ellipsis;
-      white-space: nowrap;
-    }
-  }
-  
-  .loading-text {
-    text-align: center;
-    color: #999;
-    font-size: 28rpx;
-    padding: 40rpx;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    gap: 16rpx;
-  }
-  
-  .empty-text {
-    text-align: center;
-    color: #999;
-    font-size: 28rpx;
-    padding: 80rpx 40rpx;
-  }
+	position: relative;
+	height: 60vh;
+	padding: 40rpx 0rpx;
+	box-sizing: border-box;
+	display: flex;
+	flex-direction: column;
+
+	.popup-header {
+		text-align: center;
+		font-weight: 600;
+		margin-bottom: 20rpx;
+	}
+
+	.list-container {
+		flex: 1;
+		min-height: 0;
+		width: 100%;
+		overflow: hidden;
+	}
+
+	.viewer-item {
+		align-items: center;
+		padding: 16rpx 0;
+
+		.rank-number {
+			text-align: center;
+			margin-right: 20rpx;
+			font-size: 28rpx;
+		}
+
+		.avatar-container {
+			.default-avatar {
+				width: 72rpx;
+				height: 72rpx;
+				border-radius: 50%;
+				display: flex;
+				align-items: center;
+				justify-content: center;
+
+				.avatar-text {
+					color: #ffffff;
+					font-size: 24rpx;
+					font-weight: 500;
+				}
+			}
+		}
+
+		.nickname {
+			flex: 1;
+			overflow: hidden;
+			text-overflow: ellipsis;
+			white-space: nowrap;
+		}
+	}
+
+	.loading-text {
+		text-align: center;
+		color: #999;
+		font-size: 28rpx;
+		padding: 40rpx;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		gap: 16rpx;
+	}
+
+	.empty-text {
+		text-align: center;
+		color: #999;
+		font-size: 28rpx;
+		padding: 80rpx 40rpx;
+	}
+	
+	.more-text {
+		text-align: center;
+		color: #999;
+		font-size: 28rpx;
+		padding: 30rpx 20rpx;
+	}
+
+	.list-container {
+		flex: 1;
+		min-height: 0;
+		overflow: hidden;
+	}
 }
-</style>
+</style>

BIN
pages_course/living 2.vue.zip


+ 136 - 102
pages_course/living.vue

@@ -31,6 +31,7 @@
 							</view>
 						</view>
 					</view>
+					
 					<view class="end">
 						<view v-if="Array.isArray(filteredViewers)" class="align-center" @click="toggleViewerList"
 							style="margin-top: 88rpx">
@@ -44,6 +45,7 @@
 							</view>
 							<view class="sum">{{ formattedWatchCount || 0 }}</view>
 						</view>
+						
 						<view class="complaint-box" @click="navgetTo('/pages_shopping/live/complaintList')">
 							<image class="image w32 h32 mr10" src="/static/images/complaint.png" mode="widthFix" />
 							<view class="fs26">投诉</view>
@@ -66,9 +68,8 @@
 
 				<!-- 右边的side -->
 				<view v-show="!isFocus" class="side-group" :class="{
-            'top2': (!isShowRed && !(isShowLottery && countdown)),
-            'top3': (isShowRed || (isShowLottery && countdown))
-          }">
+                  'top2': (!isShowRed && !(isShowLottery && countdown)),
+                  'top3': (isShowRed || (isShowLottery && countdown))}">
 					<view class="side-item" @click="onRed()" v-if="isShowRed">
 						<button class="button button-reset" style="height: 70rpx;">
 							<image class="image" style="width: 72rpx;" src="/static/images/redbag.png"
@@ -187,8 +188,7 @@
 					<view class="integral-header">
 						<view class="integral-title">观看视频领积分</view>
 						<image class="integral-background-image"
-							src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/integral_bg.png"
-							mode="widthFix" />
+							src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/integral_bg.png"  mode="widthFix" />
 					</view>
 					<view class="integral-content">
 						<view class="integral-message">积分发放成功</view>
@@ -235,8 +235,8 @@
 				@fill-address="handleFillAddress" />
 
 			<!-- 观众列表弹窗 -->
-			<Viewer :show="showViewerList" :viewers="liveViewers" :loading="viewLoading" :scrollHeight="scrollHeight"
-				@close="closeViewerList" @open="openViewerList" @scrolltolower="handleScrollToLower" />
+			<Viewer ref="viewer" :show="showViewerList" :viewers="liveViewers" :lookAudsCount="lookAudsCount" :loading="viewLoading" :scrollHeight="scrollHeight"
+				@close="closeViewerList" @open="openViewerList" @loadMore="handleViewerLoadMore" />
 
 			<!-- 更多弹窗 -->
 			<u-popup :show="isMore" @close="closeMore" round="20rpx" bgColor="#f3f5f9" zIndex="10076">
@@ -463,6 +463,7 @@
 				pingTimeoutTimer: null,
 				heartBeatTimer: null,
 				liveViewDataTimer: null,
+				lookAudsCount:0,//更多的观众人数
 				reconnectTimer: null,
 				scrollTimer: null,
 				lastScrollTime: 0,
@@ -493,6 +494,7 @@
 				isConnecting: false,
 				hasInitialized: false,
 				liveViewersData: [],
+				liveTopViewersData: [],
 				liveUserCalled: false,
 				userRandomColors: Object.create(null),
 				heartBeatRetryCount: 0,
@@ -574,14 +576,14 @@
 				liveId: null,
 				userinfo: '',
 				userData: {},
-				diffTotalTime: ''
+				diffTotalTime: '',
+				virtualTotal:0,
 			}
 		},
 		async onLoad(options) {
 			if (options.liveId) {
 				this.liveId = options.liveId
 			}
-
 			if (options.scene) {
 				this.scene = options.scene
 				const decodedScene = decodeURIComponent(this.scene)
@@ -599,7 +601,6 @@
 			if (options.companyId && options.companyUserId) {
 				this.qrFrom = `&companyId=${options.companyId}&companyUserId=${options.companyUserId}`
 			}
-
 			this.userinfo = uni.getStorageSync('userInfo')
 			this.userData = uni.getStorageSync('userData')
 			this.hasSubscribed = uni.getStorageSync('subscribe_status_' + this.liveId) || false;
@@ -626,6 +627,11 @@
 			} catch (error) {
 				console.error('初始化失败:', error)
 			}
+			
+			this.startMemoryMonitor();
+			
+			//获取礼物列表
+			this.getActiveList();
 
 			uni.onKeyboardHeightChange((res) => {
 				console.log('键盘高度变化:', res.height, '平台:', this.systemInfo.platform)
@@ -679,16 +685,13 @@
 			this.getUserIntegralInfo();
 		},
 		onPullDownRefresh() {
-			this.getLiveMsg(this.liveItem)
-			this.getliveUser()
+			this.getLiveMsg(this.liveItem);
+			this.getliveUserInit();
 			setTimeout(() => {
 				uni.stopPullDownRefresh()
 			}, 1000)
 		},
 		async onShow() {
-
-
-			this.getActiveList()
 			try {
 				const isLogin = await this.utils.isLogin()
 				if (isLogin) {
@@ -704,17 +707,12 @@
 			} catch (error) {
 				console.error('初始化失败:', error)
 			}
-
 			if (this.liveId) {
 				await this.getLiveMsg(this.liveItem)
 			}
-
 			if (!this.liveViewData) {
 				this.getliveViewData()
 			}
-
-			await this.getUserInfo()
-
 			this.uuId = generateRandomString(16)
 			const isLiveLogin = uni.getStorageSync('isLiveLogin')
 			this.share = uni.getStorageSync('share')
@@ -759,16 +757,14 @@
 				if (this.$refs.liveVideo && this.$refs.liveVideo.setVideoProgress) {
 					this.$refs.liveVideo.setVideoProgress()
 				}
-			})
-
+			});
 			if (this.lookTimer) {
 				clearInterval(this.lookTimer)
 				this.lookTimer = null
 				this.stayTime = 0
 				this.startTime = 0
 			}
-
-			this.startMemoryMonitor()
+			
 
 			if (this.trafficTimer) {
 				clearInterval(this.trafficTimer)
@@ -831,7 +827,7 @@
 				return this.formatNumber(this.liveViewData.like || 0)
 			},
 			filteredViewers() {
-				const safeLiveViewers = Array.isArray(this.liveViewersData) ? this.liveViewersData : []
+				const safeLiveViewers = Array.isArray(this.liveTopViewersData) ? this.liveTopViewersData : []
 				return safeLiveViewers.slice(0, 3)
 			},
 			isCurrentUserWon() {
@@ -873,7 +869,6 @@
 			if (this.$refs.liveVideo && this.$refs.liveVideo.saveVideoProgress) {
 				this.$refs.liveVideo.saveVideoProgress()
 			}
-
 			if (this.liveItem) {
 				if (this.$refs.liveVideo && this.$refs.liveVideo.pauseVideo) {
 					this.$refs.liveVideo.pauseVideo()
@@ -888,26 +883,22 @@
 			this.stopCountdown();
 			this.closeWebSocket(true);
 			this.clearAllTimersEnhanced();
-
 			const videoId = `myVideo_${this.liveId}`
 			const videoContext = uni.createVideoContext(videoId, this)
 			if (videoContext) {
 				videoContext.pause();
 			}
-
 			try {
 				uni.offNetworkStatusChange();
 			} catch (err) {
 				console.warn('移除网络状态监听失败:', err)
 			}
-
 			this.clearBigData();
 			this.resetAllStates();
 		},
 		mounted() {
 			this.systemInfo = uni.getSystemInfoSync()
 			console.log('系统信息:', this.systemInfo.platform, this.systemInfo.model)
-
 			this.getCurrentActivities()
 			this.getliveOrder()
 		},
@@ -926,7 +917,6 @@
 			getUserIntegralInfo() {
 				getUserIntegralInfo().then(res => {
 					if (res.code == 200) {
-
 						this.integralNum = res.data.integral
 					}
 				}).catch(error => {
@@ -1471,7 +1461,6 @@
 					videoContext.pause()
 					setTimeout(() => {
 						this.$set(this.liveItem, 'videoUrl', this.liveItem.videoUrl + '&t=' + Date.now())
-
 						setTimeout(() => {
 							if (videoContext.seek) {
 								videoContext.seek(currentTime)
@@ -1712,8 +1701,7 @@
 					'reconnectTimer', 'scrollTimer', 'searchTimer', 'purchasePromptTimer',
 					'welcomeTimer', 'redTimer', 'liveStartTimer', 'lotteryTimer', 'noticeTimer',
 					'memoryMonitorTimer', 'networkStatusTimer', 'networkRetryTimer'
-				]
-
+				];
 				return timers.filter(timer => this[timer] !== null).length
 			},
 			cleanupUnusedTimers() {
@@ -1722,8 +1710,7 @@
 					'reconnectTimer', 'scrollTimer', 'searchTimer', 'purchasePromptTimer',
 					'welcomeTimer', 'redTimer', 'liveStartTimer', 'lotteryTimer',
 					'networkStatusTimer', 'networkRetryTimer'
-				]
-
+				];
 				timers.forEach(timerName => {
 					if (this[timerName] && typeof this[timerName] === 'number') {
 						if (timerName.includes('Interval')) {
@@ -1761,8 +1748,8 @@
 				})
 			},
 			preventDoubleClick(e) {
-				e.preventDefault()
-				e.stopPropagation()
+				e.preventDefault();
+				e.stopPropagation();
 				return false
 			},
 			clearAllTimers() {
@@ -1822,7 +1809,7 @@
 				}
 				this.memoryMonitorTimer = setInterval(() => {
 					this.checkAndCleanMemory()
-				}, 5 * 60 * 1000)
+				}, 5 * 60 * 1000);
 			},
 			checkAndCleanMemory() {
 				try {
@@ -1918,8 +1905,7 @@
 				return nickName[0].toUpperCase()
 			},
 			async getUserInfo() {
-				await getUserInfo().then(
-					(res) => {
+				await getUserInfo().then((res) => {
 						if (res.code == 200) {
 							this.userData = res.user
 						} else {
@@ -2519,54 +2505,48 @@
 				const totalTime = this.calculateTimeDiff(item)
 				item.timeTimer = setInterval(() => {
 					const totalTime = this.calculateTimeDiff(item)
-				}, 1000)
+				}, 1000);
 			},
-			toggleViewerList() {
+			toggleViewerList() {  //加载直播间观众
 				const now = Date.now()
 				if (now - this.lastClickTime > this.clickDelay) {
 					this.showViewerList = !this.showViewerList
-					if (this.showViewerList) {
-						this.getliveUser(false)
-					}
-					this.lastClickTime = now
+					// if (this.showViewerList) {
+					// 	this.getliveUser(false);
+					// }
+					this.lastClickTime = now;
 				}
 			},
 			closeViewerList() {
 				this.showViewerList = false
 			},
 			openViewerList() {
-				this.$nextTick(() => {
-					this.calculateScrollHeight()
-				})
+				// this.$nextTick(() => {
+				// 	this.calculateScrollHeight()
+				// })
 			},
 			calculateScrollHeight() {
-				const query = uni.createSelectorQuery().in(this)
-				query.select('.viewer-list-popup').boundingClientRect((data) => {
-					if (data) {
-						this.scrollHeight = data.height - 120
-					}
-				}).exec()
+				// const query = uni.createSelectorQuery().in(this)
+				// query.select('.viewer-list-popup').boundingClientRect((data) => {
+				// 	if (data) {
+				// 		this.scrollHeight = data.height - 120
+				// 	}
+				// }).exec()
 			},
 			openViews() {
 				this.$nextTick(() => {
 					const query = uni.createSelectorQuery().in(this)
-					query
-						.select('.view-box')
-						.boundingClientRect((data) => {
+					query.select('.view-box').boundingClientRect((data) => {
 							if (data) {
 								this.scrollHeight = data.height - 80
 							}
-						})
-						.exec()
+						}).exec();
 				})
 			},
-			handleScrollToLower() {
-				if (this.scrollTimer) {
-					clearTimeout(this.scrollTimer)
-				}
-				this.scrollTimer = setTimeout(() => {
-					this.getliveUser(true)
-				}, 1000)
+			async handleViewerLoadMore(page) {
+				if (this.viewLoading) return;
+				this.viewPageNum = page.num;
+				await this.getliveUser(page.num > 1);
 			},
 			async getLiveMsg(liveItem) {
 				if (!liveItem || !this.liveId) {
@@ -2618,44 +2598,99 @@
 				}
 			},
 			async getliveUser(isLoadMore = false) {
-				this.viewLoading = true
+				if (!isLoadMore) {
+					this.liveViewers = []
+				}
+				this.viewLoading = true;
 				try {
-					const res = await watchUserList(this.liveId, this.viewPageSize, this.viewPageNum, false)
+					const res = await watchUserList(this.liveId, this.viewPageSize, this.viewPageNum, false);
+					console.log("qxj watchUserList res",res);
 					if (res.code === 200) {
-						const userRows = Array.isArray(res.rows) ? res.rows : []
-
-						let array = userRows.map((item) => ({
-							avatar: item.avatar || '',
-							userId: item.userId || '',
-							nickName: item.nickName || '未命名'
-						}))
-
-						let virtualData = []
-						let virtualTotal = res.total * 2 + 50
-						this.liveUserTotal = virtualTotal
+						const newRows = Array.isArray(res.rows) ? res.rows : []
+						
+						const newViewers = newRows.map((item) => ({
+								avatar: item.avatar || '',
+								userId: item.userId || '',
+								nickName: item.nickName || '未命名'
+						}));
+
+						let virtualData = [];
+						let virtualTotal = res.total > 20 ? 20 : res.total;
 						for (let i = 0; i < virtualTotal; i++) {
 							let data = {
 								avatar: '',
 								userId: '8565' + i,
 								nickName: this.generateRandomChineseName()
 							}
-							virtualData.push(data)
+							virtualData.push(data);
+						}
+						
+						if (!isLoadMore) {
+							this.liveViewersData = [...newViewers, ...virtualData];
 						}
-						this.liveViewersData = [...array, ...virtualData]
-
-						const newRows = Array.isArray(res.rows) ? res.rows : []
-						const currentViewers = Array.isArray(this.liveViewers) ? this.liveViewers : []
 
-						let viewlist = isLoadMore ? [...currentViewers, ...newRows] : newRows
-						this.liveViewers = [...viewlist, ...virtualData]
-						this.viewPageNum++
+						// 过滤掉旧列表中的虚拟数据,保留真实用户
+						let currentRealViewers = isLoadMore ? this.liveViewers.filter(v => !String(v.userId).startsWith('8565')) : [];
+						
+						// 合并真实用户
+						let allRealViewers = [...currentRealViewers, ...newViewers];
+						
+						// 加上虚拟用户放在最后
+						this.liveViewers = [...allRealViewers, ...virtualData];
+						
+						let hasMore = newRows.length > 0;
+						if (this.liveViewers.length >= 100) {
+							hasMore = false;
+							this.lookAudsCount = this.liveUserTotal - 100;
+						} else {
+							this.lookAudsCount = 0;
+						}
+						
+						if (this.$refs.viewer) {
+							this.$refs.viewer.endSuccess(newRows.length, hasMore);
+						}
+					} else {
+						if (this.$refs.viewer) this.$refs.viewer.endErr();
 					}
 				} catch (error) {
 					console.error('获取观众列表失败:', error)
+					if (this.$refs.viewer) this.$refs.viewer.endErr();
 				} finally {
 					this.viewLoading = false
 				}
 			},
+			async getliveUserInit(isLoadMore = false) {
+				try {
+					const res = await watchUserList(this.liveId, this.viewPageSize, 1, false);
+					console.log("qxj watchUserList res",res);
+					if (res.code === 200) {
+						const userRows = Array.isArray(res.rows) ? res.rows : []
+						let array = userRows.map((item) => ({
+							avatar: item.avatar || '',
+							userId: item.userId || '',
+							nickName: item.nickName || '未命名'
+						}));
+						let virtualData = [];
+						let virtualTotal = res.total * 2 + 50;
+						//this.virtualTotal= virtualTotal;
+						this.liveUserTotal = virtualTotal;
+						// for (let i = 0; i < 5; i++) {
+						// 	let data = {
+						// 		avatar: '',
+						// 		userId: '8565' + i,
+						// 		nickName: this.generateRandomChineseName()
+						// 	}
+						// 	virtualData.push(data)
+						// }
+						this.liveTopViewersData = [...array];
+					
+					}
+				} catch (error) {
+					console.error('获取观众列表失败:', error)
+				} finally {
+				}
+			},
+			
 			showPurchaseMessage() {
 				if (this.purchasePromptTimer) {
 					clearTimeout(this.purchasePromptTimer)
@@ -2979,7 +3014,7 @@
 				this.resetReconnectState()
 
 				setTimeout(() => {
-					this.getliveUser(false)
+					this.getliveUserInit(false)
 				}, 200)
 
 				const now = new Date()
@@ -3264,14 +3299,13 @@
 						} else if (socketMessage.cmd == 'globalVisible') {} else if (socketMessage.cmd ==
 							'singleVisible') {} else if (socketMessage.cmd == 'entry') {
 							try {
-								if (!this.liveUserCalled) {
-									await this.getliveUser(false)
-									this.liveUserCalled = true
-								}
+								//if (!this.liveUserCalled) {
+									await this.getliveUserInit(false)
+									this.liveUserCalled = true;
+								//}
 								const userIdToEntry = socketMessage.userId
 								const existingIndex = this.liveViewersData.findIndex((item) => item.userId ===
-									userIdToEntry)
-
+userIdToEntry);
 								if (existingIndex === -1) {
 									const liveViewers = {
 										userId: socketMessage.userId,
@@ -3281,16 +3315,16 @@
 									this.liveViewersData.push(liveViewers)
 									this.liveUserTotal++
 								}
-
+								
+								
 								const userData = JSON.parse(socketMessage.data || '{}')
 								const userId = userData.userId || socketMessage.userId
 								if (!userId) return
-
+								
 								if (!this.shownEntryUsers.has(userId)) {
-									this.inAndOut = socketMessage
-									this.showWelcomeMessage = true
-									this.shownEntryUsers.add(userId)
-
+									this.inAndOut = socketMessage;
+									this.showWelcomeMessage = true;
+									this.shownEntryUsers.add(userId);
 									if (this.welcomeTimer) clearTimeout(this.welcomeTimer)
 									this.welcomeTimer = setTimeout(() => {
 										this.showWelcomeMessage = false

BIN
pages_course/living.vue.zip


+ 2 - 1
pages_course/livingList.vue

@@ -100,7 +100,8 @@
 			goLive(item) {
 				uni.navigateTo({
 					// &immediate=true
-					url: `./living?liveId=${item.liveId}`
+					url: './living?liveId=945'
+					//url: `./living?liveId=${item.liveId}`
 				});
 			}
 		}

+ 116 - 0
ws-load-test.js

@@ -0,0 +1,116 @@
+// WebSocket 并发压测(模拟进入直播间+心跳)
+const WebSocket = require('ws');
+const crypto = require('crypto');
+
+const WS_HOST = process.env.WS_HOST || 'wss://im.fhhx.runtzh.com/ws/app/webSocket'; 
+const LIVE_ID = process.env.LIVE_ID;                    // 必填:直播间ID
+const USER_TYPE = parseInt(process.env.USER_TYPE || '0', 10);    // 与前端一致,默认0
+const HEARTBEAT_MS = parseInt(process.env.HB || '15000', 10);    // 心跳间隔
+const RATE = parseInt(process.env.RATE || '80', 10);             // 每秒建链速率
+const DURATION_SEC = parseInt(process.env.DURATION || '600', 10);// 运行总时长(秒)
+
+// 新增:可选 Origin 和扫码参数
+const ORIGIN = process.env.ORIGIN || 'https://im.fhhx.runtzh.com';
+const COMPANY_ID = process.env.COMPANY_ID;
+const COMPANY_USER_ID = process.env.COMPANY_USER_ID;
+
+const UID_START = parseInt(process.env.UID_START || '900000000', 10);
+const UID_TO = parseInt(process.env.UID_TO || '0', 10);
+let COUNT = parseInt(process.env.COUNT || '0', 10);
+
+if (!LIVE_ID) {
+  console.error('缺少 LIVE_ID。示例:LIVE_ID=359 node scripts/ws-load-test.js');
+  process.exit(1);
+}
+if (UID_TO > 0 && UID_TO >= UID_START) {
+  COUNT = UID_TO - UID_START + 1;
+}
+if (!COUNT || COUNT <= 0) {
+  console.error('缺少 COUNT 或 UID_TO,无法确定连接数量');
+  process.exit(1);
+}
+
+function buildUrl(userId) {
+  const ts = Date.now();
+  const message = `${LIVE_ID}${userId}${USER_TYPE}${ts}`;
+  const signature = crypto.createHmac('sha256', String(ts)).update(message).digest('hex');
+  let url = `${WS_HOST}?userId=${userId}&liveId=${LIVE_ID}&userType=${USER_TYPE}&timestamp=${ts}&signature=${signature}`;
+  // 新增:可选扫码来源参数
+  if (COMPANY_ID && COMPANY_USER_ID) {
+    url += `&companyId=${encodeURIComponent(COMPANY_ID)}&companyUserId=${encodeURIComponent(COMPANY_USER_ID)}`;
+  }
+  return url;
+}
+
+const stats = { open: 0, close: 0, error: 0, msg: 0 };
+
+function startClient(index) {
+  const userId = String(UID_START + index);
+  const url = buildUrl(userId);
+  // 新增:传入 origin 头,避免被服务端拒绝
+  const ws = new WebSocket(url, { perMessageDeflate: false, origin: ORIGIN });
+  let hbTimer = null;
+
+  ws.on('open', () => {
+    stats.open++;
+    console.log(`[${userId}] connected`);
+    const sendHb = () => {
+      const now = Date.now();
+      const payload = JSON.stringify({
+        cmd: 'heartbeat',
+        msg: 'ping',
+        userId,
+        liveId: LIVE_ID,
+        timestamp: now,
+        networkType: 'wifi',
+      });
+      ws.send(payload);
+    };
+    // 新增:打开即发送一次心跳, subsequent 定时发送
+    sendHb();
+    hbTimer = setInterval(sendHb, HEARTBEAT_MS);
+  });
+
+  // 新增:打印服务端返回的 cmd,观察是否有 heartbeat/entry/out 等
+  ws.on('message', (msg) => {
+    stats.msg++;
+    try {
+      const parsed = JSON.parse(msg);
+      if (parsed && parsed.data && parsed.data.cmd) {
+        console.log(`[${userId}] recv cmd=${parsed.data.cmd}`);
+      }
+    } catch (_) {}
+  });
+
+  ws.on('error', (err) => {
+    stats.error++;
+    console.error(`[${userId}] error`, err?.message || err);
+    if (hbTimer) clearInterval(hbTimer);
+  });
+
+  ws.on('close', (code, reason) => {
+    stats.close++;
+    console.log(`[${userId}] closed code=${code} reason=${reason || ''}`);
+    if (hbTimer) clearInterval(hbTimer);
+  });
+}
+
+async function run() {
+  console.log(`开始压测: liveId=${LIVE_ID}, count=${COUNT}, rate=${RATE}/s, hb=${HEARTBEAT_MS}ms, uid=[${UID_START}..${UID_START + COUNT - 1}]`);
+  for (let i = 0; i < COUNT; i++) {
+    startClient(i);
+    const delayMs = Math.max(1, Math.floor(1000 / RATE));
+    await new Promise((r) => setTimeout(r, delayMs));
+  }
+  const endAt = Date.now() + DURATION_SEC * 1000;
+  const t = setInterval(() => {
+    console.log(`[stats] open=${stats.open} close=${stats.close} error=${stats.error} msg=${stats.msg}`);
+    if (Date.now() >= endAt) {
+      console.log('压测结束。');
+      clearInterval(t);
+      process.exit(0);
+    }
+  }, 2000);
+}
+
+run().catch((e) => { console.error('运行异常', e); process.exit(1); });