管理端页面补充

This commit is contained in:
2025-06-28 17:07:18 +08:00
parent 7fcff7759d
commit 240dd5d2a4
16 changed files with 2106 additions and 165 deletions
+486
View File
@@ -11,6 +11,7 @@
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^13.5.0",
"@twilio/conversations": "^2.6.2",
"@types/jest": "^27.5.2",
"@types/node": "^16.11.56",
"@types/react": "^18.0.17",
@@ -21,6 +22,8 @@
"react-dom": "^18.2.0",
"react-router-dom": "^6.4.0",
"react-scripts": "5.0.1",
"twilio": "^5.7.1",
"twilio-video": "^2.31.0",
"typescript": "^4.7.4",
"web-vitals": "^2.1.4"
},
@@ -3594,6 +3597,146 @@
"node": ">=10.13.0"
}
},
"node_modules/@twilio/conversations": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@twilio/conversations/-/conversations-2.6.2.tgz",
"integrity": "sha512-xbikMRIiDeVxthchThAp2aL3BDHxCd6gqDkpQU4cwMHkzktxceuTC3NoBmV6HtDHzt1xjvct2WxBKw/ZPJj8Xw==",
"dependencies": {
"@babel/runtime": "^7.17.0",
"@twilio/declarative-type-validator": "^0.2.10",
"@twilio/deprecation-decorator": "^0.2.8",
"@twilio/mcs-client": "^0.6.10",
"@twilio/notifications": "^2.0.9",
"@twilio/operation-retrier": "^4.0.18",
"@twilio/replay-event-emitter": "^0.3.10",
"core-js": "^3.17.3",
"iso8601-duration": "=1.2.0",
"isomorphic-form-data": "^2.0.0",
"lodash.isequal": "^4.5.0",
"loglevel": "^1.8.0",
"platform": "^1.3.6",
"quick-lru": "^5.1.1",
"twilio-sync": "~3.1.0",
"twilsock": "~0.12.2",
"uuid": "^3.4.0",
"xmlhttprequest": "^1.8.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@twilio/conversations/node_modules/uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
"bin": {
"uuid": "bin/uuid"
}
},
"node_modules/@twilio/declarative-type-validator": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/@twilio/declarative-type-validator/-/declarative-type-validator-0.2.10.tgz",
"integrity": "sha512-q3ep+qsctZ0u+xr1U6/kjs4RlIJ/u8+wHyUQkrNCoVtjgnCV938P0RP7OUkW/fYt/vLhfy0Nnzo1G0bBbn9o/w==",
"dependencies": {
"@babel/runtime": "^7.17.0",
"core-js": "^3.17.3"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@twilio/deprecation-decorator": {
"version": "0.2.8",
"resolved": "https://registry.npmjs.org/@twilio/deprecation-decorator/-/deprecation-decorator-0.2.8.tgz",
"integrity": "sha512-kDWN6sxOisTMQXQL0JgvzqGjNZIUj+hdZ1eZtm/5z6EMb14xuoi6qSXb//0x6Yn02zSrr6c7K8YKDrUawDRabQ==",
"dependencies": {
"@babel/runtime": "^7.17.0",
"core-js": "^3.17.3",
"loglevel": "1.8.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@twilio/deprecation-decorator/node_modules/loglevel": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.0.tgz",
"integrity": "sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA==",
"engines": {
"node": ">= 0.6.0"
},
"funding": {
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/loglevel"
}
},
"node_modules/@twilio/mcs-client": {
"version": "0.6.10",
"resolved": "https://registry.npmjs.org/@twilio/mcs-client/-/mcs-client-0.6.10.tgz",
"integrity": "sha512-MrZtvxyChUXUpcHG+BR/hY3u573/HsBtMMrTQE4HRvy3v9ZYY0RhHrECqW2TRw0iA3R9quk0CNrz357fjg8I+w==",
"dependencies": {
"@babel/runtime": "^7.17.0",
"@twilio/declarative-type-validator": "^0.2.10",
"@twilio/operation-retrier": "^4.0.18",
"core-js": "^3.17.3",
"loglevel": "^1.8.0",
"xmlhttprequest": "^1.8.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@twilio/notifications": {
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/@twilio/notifications/-/notifications-2.0.9.tgz",
"integrity": "sha512-sHuiIwSPx9xMo6y2l1p+lISs7kWt299B7AOsFqhoetgQr/Pz2PePwKLDruch9OWtW8ZNnT/vOpoz7qbN15vvmA==",
"dependencies": {
"@babel/runtime": "^7.17.0",
"@twilio/declarative-type-validator": "^0.2.10",
"@twilio/operation-retrier": "^4.0.18",
"core-js": "^3.17.3",
"loglevel": "^1.8.0",
"twilsock": "~0.12.2",
"uuid": "^3.4.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@twilio/notifications/node_modules/uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
"bin": {
"uuid": "bin/uuid"
}
},
"node_modules/@twilio/operation-retrier": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@twilio/operation-retrier/-/operation-retrier-4.0.18.tgz",
"integrity": "sha512-vG3i41XEa4lyC3+8FRFbjYBPZQftkI1WrJTtTDBf85N2UzZ8brqrUp9EbSdQmny1/zIvoZ18AswQrvBDLtNEvA==",
"dependencies": {
"@babel/runtime": "^7.17.0",
"core-js": "^3.17.3"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@twilio/replay-event-emitter": {
"version": "0.3.10",
"resolved": "https://registry.npmjs.org/@twilio/replay-event-emitter/-/replay-event-emitter-0.3.10.tgz",
"integrity": "sha512-GaT5ihN3eJvIgCV81ggvLgtNwMtD2pBR4hMrFE/dlqN73FsNw01bawLSMIofOdRj8ydEPgvSTJHVCIjBWKHQvg==",
"dependencies": {
"@babel/runtime": "^7.17.0",
"core-js": "^3.17.3"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@types/aria-query": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
@@ -4847,6 +4990,11 @@
"node": ">= 0.4"
}
},
"node_modules/async-limiter": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -4918,6 +5066,31 @@
"node": ">=4"
}
},
"node_modules/axios": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axios/node_modules/form-data": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz",
"integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -5338,6 +5511,11 @@
"node-int64": "^0.4.0"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -6736,6 +6914,14 @@
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -9533,6 +9719,34 @@
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
},
"node_modules/iso8601-duration": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/iso8601-duration/-/iso8601-duration-1.2.0.tgz",
"integrity": "sha512-ErTBd++b17E8nmWII1K1uZtBgD1E8RjyvwmxlCjPHNqHMD7gmcMHOw0E8Ro/6+QT4PhHRSnnMo7bxa1vFPkwhg=="
},
"node_modules/isomorphic-form-data": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isomorphic-form-data/-/isomorphic-form-data-2.0.0.tgz",
"integrity": "sha512-TYgVnXWeESVmQSg4GLVbalmQ+B4NPi/H4eWxqALKj63KsUrcu301YDjBqaOw3h+cbak7Na4Xyps3BiptHtxTfg==",
"dependencies": {
"form-data": "^2.3.2"
}
},
"node_modules/isomorphic-form-data/node_modules/form-data": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.3.tgz",
"integrity": "sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"mime-types": "^2.1.35",
"safe-buffer": "^5.2.1"
},
"engines": {
"node": ">= 0.12"
}
},
"node_modules/istanbul-lib-coverage": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
@@ -9671,6 +9885,11 @@
"node": ">=10"
}
},
"node_modules/javascript-state-machine": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/javascript-state-machine/-/javascript-state-machine-3.1.0.tgz",
"integrity": "sha512-BwhYxQ1OPenBPXC735RgfB+ZUG8H3kjsx8hrYTgWnoy6TPipEy4fiicyhT2lxRKAXq9pG7CfFT8a2HLr6Hmwxg=="
},
"node_modules/jest": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz",
@@ -10682,6 +10901,27 @@
"node": ">=0.10.0"
}
},
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
"dependencies": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jsx-ast-utils": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@@ -10696,6 +10936,25 @@
"node": ">=4.0"
}
},
"node_modules/jwa": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
"integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"dependencies": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -10828,6 +11087,42 @@
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead."
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
},
"node_modules/lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -10838,6 +11133,11 @@
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
},
"node_modules/lodash.sortby": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
@@ -10848,6 +11148,18 @@
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
"integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="
},
"node_modules/loglevel": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz",
"integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==",
"engines": {
"node": ">= 0.6.0"
},
"funding": {
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/loglevel"
}
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -11774,6 +12086,11 @@
"node": ">=4"
}
},
"node_modules/platform": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="
},
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -13123,6 +13440,11 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/psl": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
@@ -13190,6 +13512,17 @@
}
]
},
"node_modules/quick-lru": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
@@ -14683,6 +15016,11 @@
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"node_modules/scmp": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz",
"integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q=="
},
"node_modules/scroll-into-view-if-needed": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
@@ -16154,6 +16492,126 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/twilio": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/twilio/-/twilio-5.7.1.tgz",
"integrity": "sha512-BcoVK6FR580HRX94z2u3b+foHkvFj39DDzLU4Xv+N/7ejDIGgQdrtg7CgRqIT04UNs98HJAvjuAOzkYetI6ExQ==",
"dependencies": {
"axios": "^1.8.3",
"dayjs": "^1.11.9",
"https-proxy-agent": "^5.0.0",
"jsonwebtoken": "^9.0.2",
"qs": "^6.9.4",
"scmp": "^2.1.0",
"xmlbuilder": "^13.0.2"
},
"engines": {
"node": ">=14.0"
}
},
"node_modules/twilio-sync": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/twilio-sync/-/twilio-sync-3.1.0.tgz",
"integrity": "sha512-KNkbbnoBITpsmxV2UnmNDEot/Q5t7p5I1zP05oqj0OYT1kMcZq4nhiSNkcxkunfxINFSUzz8d/mUA82yWS7iLQ==",
"dependencies": {
"@babel/runtime": "^7.14.5",
"@twilio/declarative-type-validator": "^0.1.11",
"@twilio/operation-retrier": "^4.0.7",
"core-js": "^3.17.3",
"iso8601-duration": "=1.2.0",
"loglevel": "^1.6.3",
"platform": "^1.3.6",
"twilsock": "^0.12.2",
"uuid": "^3.4.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/twilio-sync/node_modules/@twilio/declarative-type-validator": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/@twilio/declarative-type-validator/-/declarative-type-validator-0.1.11.tgz",
"integrity": "sha512-yRAMLPD8j3k67UFvPeZvfTlKYuceiNq+iZ8a/ADzAbZMeaV0FMvsJmG97MH8yN/VdXY9hcscchsnc99bJ1sClw==",
"dependencies": {
"@babel/runtime": "^7.14.5",
"core-js": "^3.17.3"
},
"engines": {
"node": ">=14"
}
},
"node_modules/twilio-sync/node_modules/uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
"bin": {
"uuid": "bin/uuid"
}
},
"node_modules/twilio-video": {
"version": "2.31.0",
"resolved": "https://registry.npmjs.org/twilio-video/-/twilio-video-2.31.0.tgz",
"integrity": "sha512-f0MGHQvlYT7AXbQv1G221thcokRsHrbgTYD7sa53p1QQOoGUZLco5XTM+D553lhIrmnJ2tnrtL+LWeioeY7VOQ==",
"dependencies": {
"events": "^3.3.0",
"util": "^0.12.4",
"ws": "^7.4.6",
"xmlhttprequest": "^1.8.0"
},
"engines": {
"node": ">=0.12"
}
},
"node_modules/twilsock": {
"version": "0.12.2",
"resolved": "https://registry.npmjs.org/twilsock/-/twilsock-0.12.2.tgz",
"integrity": "sha512-7G59f2TCEnxcY2ZBCzaZOPmMDoxDrK9lMTiA7UvuiKca37Dljbdlu2EHI3+d7gU1JHkH5GNCmyxqJzSbZodwXA==",
"dependencies": {
"@babel/runtime": "^7.14.5",
"@twilio/declarative-type-validator": "^0.1.11",
"@twilio/operation-retrier": "^4.0.7",
"core-js": "^3.17.3",
"iso8601-duration": "=1.2.0",
"javascript-state-machine": "^3.1.0",
"loglevel": "^1.6.3",
"platform": "^1.3.6",
"uuid": "^3.4.0",
"ws": "^5.2.3"
},
"engines": {
"node": ">=14"
}
},
"node_modules/twilsock/node_modules/@twilio/declarative-type-validator": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/@twilio/declarative-type-validator/-/declarative-type-validator-0.1.11.tgz",
"integrity": "sha512-yRAMLPD8j3k67UFvPeZvfTlKYuceiNq+iZ8a/ADzAbZMeaV0FMvsJmG97MH8yN/VdXY9hcscchsnc99bJ1sClw==",
"dependencies": {
"@babel/runtime": "^7.14.5",
"core-js": "^3.17.3"
},
"engines": {
"node": ">=14"
}
},
"node_modules/twilsock/node_modules/uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
"bin": {
"uuid": "bin/uuid"
}
},
"node_modules/twilsock/node_modules/ws": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/ws/-/ws-5.2.4.tgz",
"integrity": "sha512-fFCejsuC8f9kOSu9FYaOw8CdO68O3h5v0lg4p74o8JqWpwTf9tniOD+nOB78aWoVSS6WptVUmDrp/KPsMVBWFQ==",
"dependencies": {
"async-limiter": "~1.0.0"
}
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -16431,6 +16889,18 @@
"requires-port": "^1.0.0"
}
},
"node_modules/util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
"integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
"dependencies": {
"inherits": "^2.0.3",
"is-arguments": "^1.0.4",
"is-generator-function": "^1.0.7",
"is-typed-array": "^1.1.3",
"which-typed-array": "^1.1.2"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -17303,11 +17773,27 @@
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
"integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw=="
},
"node_modules/xmlbuilder": {
"version": "13.0.2",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz",
"integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==",
"engines": {
"node": ">=6.0"
}
},
"node_modules/xmlchars": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
},
"node_modules/xmlhttprequest": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz",
"integrity": "sha512-58Im/U0mlVBLM38NdZjHyhuMtCqa61469k2YP/AaPbvCoV9aQGUpbJBj1QRm2ytRiVQBD/fsw7L2bJGDVQswBA==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+4 -1
View File
@@ -6,6 +6,7 @@
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^13.5.0",
"@twilio/conversations": "^2.6.2",
"@types/jest": "^27.5.2",
"@types/node": "^16.11.56",
"@types/react": "^18.0.17",
@@ -16,6 +17,8 @@
"react-dom": "^18.2.0",
"react-router-dom": "^6.4.0",
"react-scripts": "5.0.1",
"twilio": "^5.7.1",
"twilio-video": "^2.31.0",
"typescript": "^4.7.4",
"web-vitals": "^2.1.4"
},
@@ -46,4 +49,4 @@
"devDependencies": {
"@types/moment": "^2.13.0"
}
}
}
+51
View File
@@ -0,0 +1,51 @@
export interface TwilioConfig {
apiKey: string;
apiSecret: string;
accountSid: string;
videoServiceSid?: string;
conversationServiceSid?: string;
}
// Twilio配置
export const twilioConfig: TwilioConfig = {
apiKey: 'SK3b25e00e6914162a7cf829cffc415cb3',
apiSecret: 'PpGH298dlRgMSeGrexUjw1flczTVIw9H',
accountSid: 'AC_YOUR_ACCOUNT_SID', // 需要从Twilio控制台获取
videoServiceSid: '', // 可选:视频服务SID
conversationServiceSid: '', // 可选:对话服务SID
};
// Token服务器URL(开发环境)
export const TOKEN_SERVER_URL = process.env.NODE_ENV === 'production'
? 'https://your-production-server.com/api/twilio/token'
: 'http://localhost:3001/api/twilio/token';
// 视频配置选项
export const videoOptions = {
audio: true,
video: {
width: 640,
height: 480,
frameRate: 24,
},
bandwidthProfile: {
video: {
mode: 'collaboration' as const,
maxTracks: 10,
},
},
dominantSpeaker: true,
networkQuality: {
local: 1,
remote: 1,
},
};
// 房间类型
export enum RoomType {
GROUP = 'group',
GROUP_SMALL = 'group-small',
PEER_TO_PEER = 'peer-to-peer',
}
export default twilioConfig;
@@ -0,0 +1,129 @@
import { twilioConfig } from '../config/twilio';
export interface TokenRequest {
identity: string;
roomName: string;
apiKey?: string;
apiSecret?: string;
}
export interface TokenResponse {
token: string;
identity: string;
roomName: string;
}
// 模拟Token生成服务
// 在实际生产环境中,这应该是一个后端API服务
export class TokenService {
// 生成访问令牌
async generateAccessToken(request: TokenRequest): Promise<TokenResponse> {
try {
// 在实际应用中,这里应该调用后端API
// 这里我们创建一个模拟的token
const mockToken = this.generateMockToken(request.identity, request.roomName);
return {
token: mockToken,
identity: request.identity,
roomName: request.roomName,
};
} catch (error) {
console.error('Error generating access token:', error);
throw new Error('Failed to generate access token');
}
}
// 生成模拟Token(仅用于开发测试)
private generateMockToken(identity: string, roomName: string): string {
// 这是一个简化的JWT模拟
// 实际应用中应该使用Twilio SDK在后端生成真实的token
const header = {
typ: 'JWT',
alg: 'HS256',
cty: 'twilio-fpa;v=1'
};
const payload = {
iss: twilioConfig.apiKey,
sub: twilioConfig.accountSid,
nbf: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600, // 1小时有效期
jti: `${twilioConfig.apiKey}-${Date.now()}`,
grants: {
identity: identity,
video: {
room: roomName
}
}
};
// 在实际应用中,这里应该用正确的签名算法
const encodedHeader = btoa(JSON.stringify(header));
const encodedPayload = btoa(JSON.stringify(payload));
const signature = btoa(`${twilioConfig.apiSecret}-signature`);
return `${encodedHeader}.${encodedPayload}.${signature}`;
}
// 验证Token(模拟)
validateToken(token: string): boolean {
try {
const parts = token.split('.');
if (parts.length !== 3) return false;
const payload = JSON.parse(atob(parts[1]));
const now = Math.floor(Date.now() / 1000);
return payload.exp > now;
} catch {
return false;
}
}
// 解析Token信息
parseToken(token: string): { identity: string; roomName: string } | null {
try {
const parts = token.split('.');
if (parts.length !== 3) return null;
const payload = JSON.parse(atob(parts[1]));
return {
identity: payload.grants?.identity || '',
roomName: payload.grants?.video?.room || '',
};
} catch {
return null;
}
}
}
export const tokenService = new TokenService();
// Express.js API端点示例(如果需要真实的后端服务)
export const createTokenEndpoint = () => {
return async (req: any, res: any) => {
try {
const { identity, roomName } = req.body;
if (!identity || !roomName) {
return res.status(400).json({
error: 'Identity and roomName are required'
});
}
const tokenResponse = await tokenService.generateAccessToken({
identity,
roomName,
});
res.json(tokenResponse);
} catch (error) {
console.error('Token generation error:', error);
res.status(500).json({
error: 'Failed to generate token'
});
}
};
};
@@ -0,0 +1,194 @@
import { connect, Room, LocalVideoTrack, LocalAudioTrack, RemoteParticipant, LocalParticipant } from 'twilio-video';
import { twilioConfig, videoOptions, RoomType, TOKEN_SERVER_URL } from '../config/twilio';
export interface TwilioToken {
token: string;
identity: string;
roomName: string;
}
export interface VideoCallOptions {
roomName: string;
identity: string;
roomType?: RoomType;
audio?: boolean;
video?: boolean;
}
export interface ParticipantInfo {
identity: string;
sid: string;
isLocal: boolean;
audioEnabled: boolean;
videoEnabled: boolean;
}
export class TwilioService {
private room: Room | null = null;
private localVideoTrack: LocalVideoTrack | null = null;
private localAudioTrack: LocalAudioTrack | null = null;
// 获取访问令牌
async getAccessToken(identity: string, roomName: string): Promise<string> {
try {
const response = await fetch(`${TOKEN_SERVER_URL}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
identity,
roomName,
apiKey: twilioConfig.apiKey,
apiSecret: twilioConfig.apiSecret,
}),
});
if (!response.ok) {
throw new Error(`Token request failed: ${response.statusText}`);
}
const data = await response.json();
return data.token;
} catch (error) {
console.error('Error getting access token:', error);
throw error;
}
}
// 连接到视频房间
async connectToRoom(options: VideoCallOptions): Promise<Room> {
try {
const token = await this.getAccessToken(options.identity, options.roomName);
const connectOptions = {
...videoOptions,
name: options.roomName,
audio: options.audio ?? true,
video: options.video ?? true,
};
this.room = await connect(token, connectOptions);
// 设置事件监听器
this.setupRoomEventListeners();
return this.room;
} catch (error) {
console.error('Error connecting to room:', error);
throw error;
}
}
// 断开连接
disconnect(): void {
if (this.room) {
this.room.disconnect();
this.room = null;
}
if (this.localVideoTrack) {
this.localVideoTrack.stop();
this.localVideoTrack = null;
}
if (this.localAudioTrack) {
this.localAudioTrack.stop();
this.localAudioTrack = null;
}
}
// 切换音频
toggleAudio(): boolean {
if (this.room && this.room.localParticipant) {
const audioTrack = Array.from(this.room.localParticipant.audioTracks.values())[0];
if (audioTrack) {
if (audioTrack.track.isEnabled) {
audioTrack.track.disable();
} else {
audioTrack.track.enable();
}
return audioTrack.track.isEnabled;
}
}
return false;
}
// 切换视频
toggleVideo(): boolean {
if (this.room && this.room.localParticipant) {
const videoTrack = Array.from(this.room.localParticipant.videoTracks.values())[0];
if (videoTrack) {
if (videoTrack.track.isEnabled) {
videoTrack.track.disable();
} else {
videoTrack.track.enable();
}
return videoTrack.track.isEnabled;
}
}
return false;
}
// 获取参与者信息
getParticipants(): ParticipantInfo[] {
if (!this.room) return [];
const participants: ParticipantInfo[] = [];
// 本地参与者
const localParticipant = this.room.localParticipant;
participants.push({
identity: localParticipant.identity,
sid: localParticipant.sid,
isLocal: true,
audioEnabled: Array.from(localParticipant.audioTracks.values()).some(track => track.track.isEnabled),
videoEnabled: Array.from(localParticipant.videoTracks.values()).some(track => track.track.isEnabled),
});
// 远程参与者
this.room.participants.forEach((participant: RemoteParticipant) => {
participants.push({
identity: participant.identity,
sid: participant.sid,
isLocal: false,
audioEnabled: Array.from(participant.audioTracks.values()).some(track => track.track && track.track.isEnabled),
videoEnabled: Array.from(participant.videoTracks.values()).some(track => track.track && track.track.isEnabled),
});
});
return participants;
}
// 获取当前房间
getCurrentRoom(): Room | null {
return this.room;
}
// 设置房间事件监听器
private setupRoomEventListeners(): void {
if (!this.room) return;
this.room.on('participantConnected', (participant: RemoteParticipant) => {
console.log('Participant connected:', participant.identity);
});
this.room.on('participantDisconnected', (participant: RemoteParticipant) => {
console.log('Participant disconnected:', participant.identity);
});
this.room.on('disconnected', (room: Room) => {
console.log('Disconnected from room:', room.name);
});
this.room.on('reconnecting', (error: any) => {
console.log('Reconnecting to room...', error);
});
this.room.on('reconnected', () => {
console.log('Reconnected to room');
});
}
}
export const twilioService = new TwilioService();