feat:聊天页面代码移植
No files matched your search
@@ -60,7 +60,10 @@
|
||||
"pinia-plugin-persist-uni": "^1.2.0",
|
||||
"vue": "^3.4.21",
|
||||
"vue-i18n": "^9.1.9",
|
||||
"weixin-js-sdk": "^1.6.0"
|
||||
"weixin-js-sdk": "^1.6.0",
|
||||
"pinyin-pro": "^3.23.1",
|
||||
"quill": "^1.3.7",
|
||||
"quill-image-resize-mp": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dcloudio/types": "^3.4.8",
|
||||
|
||||
@@ -77,6 +77,15 @@ importers:
|
||||
pinia-plugin-persist-uni:
|
||||
specifier: ^1.2.0
|
||||
version: 1.3.1(pinia@2.3.1(typescript@4.9.5)(vue@3.5.25(typescript@4.9.5)))(vue@3.5.25(typescript@4.9.5))
|
||||
pinyin-pro:
|
||||
specifier: ^3.23.1
|
||||
version: 3.27.0
|
||||
quill:
|
||||
specifier: ^1.3.7
|
||||
version: 1.3.7
|
||||
quill-image-resize-mp:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1
|
||||
vue:
|
||||
specifier: ^3.4.21
|
||||
version: 3.5.25(typescript@4.9.5)
|
||||
@@ -1926,6 +1935,10 @@ packages:
|
||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
call-bind@1.0.8:
|
||||
resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
call-bound@1.0.4:
|
||||
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1970,6 +1983,10 @@ packages:
|
||||
cliui@7.0.4:
|
||||
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
|
||||
|
||||
clone@2.1.2:
|
||||
resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
|
||||
engines: {node: '>=0.8'}
|
||||
|
||||
co@4.6.0:
|
||||
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
|
||||
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
|
||||
@@ -2126,6 +2143,10 @@ packages:
|
||||
dedent@0.7.0:
|
||||
resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==}
|
||||
|
||||
deep-equal@1.1.2:
|
||||
resolution: {integrity: sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
deepmerge@4.3.1:
|
||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2134,6 +2155,14 @@ packages:
|
||||
resolution: {integrity: sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
define-data-property@1.1.4:
|
||||
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
define-properties@1.2.1:
|
||||
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
defu@6.1.4:
|
||||
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
||||
|
||||
@@ -2270,6 +2299,9 @@ packages:
|
||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
eventemitter3@2.0.3:
|
||||
resolution: {integrity: sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==}
|
||||
|
||||
execa@5.1.1:
|
||||
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -2292,6 +2324,12 @@ packages:
|
||||
exsolve@1.0.8:
|
||||
resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
|
||||
|
||||
extend@3.0.2:
|
||||
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
||||
|
||||
fast-diff@1.1.2:
|
||||
resolution: {integrity: sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==}
|
||||
|
||||
fast-glob@3.3.3:
|
||||
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
|
||||
engines: {node: '>=8.6.0'}
|
||||
@@ -2369,6 +2407,9 @@ packages:
|
||||
function-bind@1.1.2:
|
||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||
|
||||
functions-have-names@1.2.3:
|
||||
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
|
||||
|
||||
generic-names@4.0.0:
|
||||
resolution: {integrity: sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==}
|
||||
|
||||
@@ -2430,6 +2471,9 @@ packages:
|
||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
has-property-descriptors@1.0.2:
|
||||
resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
|
||||
|
||||
has-symbols@1.1.0:
|
||||
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2515,6 +2559,10 @@ packages:
|
||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
is-arguments@1.2.0:
|
||||
resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
is-arrayish@0.2.1:
|
||||
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
|
||||
|
||||
@@ -2526,6 +2574,10 @@ packages:
|
||||
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
is-date-object@1.1.0:
|
||||
resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
is-extglob@2.1.1:
|
||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2552,6 +2604,10 @@ packages:
|
||||
is-potential-custom-element-name@1.0.1:
|
||||
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
|
||||
|
||||
is-regex@1.2.1:
|
||||
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
is-stream@2.0.1:
|
||||
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2989,6 +3045,14 @@ packages:
|
||||
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
object-is@1.1.6:
|
||||
resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
object-keys@1.1.1:
|
||||
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
ofetch@1.5.1:
|
||||
resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==}
|
||||
|
||||
@@ -3028,6 +3092,9 @@ packages:
|
||||
pako@1.0.11:
|
||||
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||
|
||||
parchment@1.1.4:
|
||||
resolution: {integrity: sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==}
|
||||
|
||||
parse-bmfont-ascii@1.0.6:
|
||||
resolution: {integrity: sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==}
|
||||
|
||||
@@ -3123,6 +3190,9 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
pinyin-pro@3.27.0:
|
||||
resolution: {integrity: sha512-Osdgjwe7Rm17N2paDMM47yW+jUIUH3+0RGo8QP39ZTLpTaJVDK0T58hOLaMQJbcMmAebVuK2ePunTEVEx1clNQ==}
|
||||
|
||||
pirates@4.0.7:
|
||||
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -3255,6 +3325,16 @@ packages:
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
quill-delta@3.6.3:
|
||||
resolution: {integrity: sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==}
|
||||
engines: {node: '>=0.10'}
|
||||
|
||||
quill-image-resize-mp@3.0.1:
|
||||
resolution: {integrity: sha512-etBtUbNH3XY21PVwfclMNGkifjWKSVXei4Dc4GWVeUyxOoympcNaV+Bk7Ocjc6Y8VlDKs2tcf3zyJ9syIFL/KA==}
|
||||
|
||||
quill@1.3.7:
|
||||
resolution: {integrity: sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==}
|
||||
|
||||
range-parser@1.2.1:
|
||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -3286,6 +3366,10 @@ packages:
|
||||
regenerator-runtime@0.14.1:
|
||||
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
|
||||
|
||||
regexp.prototype.flags@1.5.4:
|
||||
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
regexpu-core@6.4.0:
|
||||
resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -3379,6 +3463,14 @@ packages:
|
||||
resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
set-function-length@1.2.2:
|
||||
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
set-function-name@2.0.2:
|
||||
resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
setprototypeof@1.2.0:
|
||||
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
||||
|
||||
@@ -6640,6 +6732,13 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
function-bind: 1.1.2
|
||||
|
||||
call-bind@1.0.8:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
es-define-property: 1.0.1
|
||||
get-intrinsic: 1.3.0
|
||||
set-function-length: 1.2.2
|
||||
|
||||
call-bound@1.0.4:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
@@ -6688,6 +6787,8 @@ snapshots:
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 7.0.0
|
||||
|
||||
clone@2.1.2: {}
|
||||
|
||||
co@4.6.0: {}
|
||||
|
||||
collect-v8-coverage@1.0.3: {}
|
||||
@@ -6801,12 +6902,33 @@ snapshots:
|
||||
|
||||
dedent@0.7.0: {}
|
||||
|
||||
deep-equal@1.1.2:
|
||||
dependencies:
|
||||
is-arguments: 1.2.0
|
||||
is-date-object: 1.1.0
|
||||
is-regex: 1.2.1
|
||||
object-is: 1.1.6
|
||||
object-keys: 1.1.1
|
||||
regexp.prototype.flags: 1.5.4
|
||||
|
||||
deepmerge@4.3.1: {}
|
||||
|
||||
default-gateway@6.0.3:
|
||||
dependencies:
|
||||
execa: 5.1.1
|
||||
|
||||
define-data-property@1.1.4:
|
||||
dependencies:
|
||||
es-define-property: 1.0.1
|
||||
es-errors: 1.3.0
|
||||
gopd: 1.2.0
|
||||
|
||||
define-properties@1.2.1:
|
||||
dependencies:
|
||||
define-data-property: 1.1.4
|
||||
has-property-descriptors: 1.0.2
|
||||
object-keys: 1.1.1
|
||||
|
||||
defu@6.1.4: {}
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
@@ -6926,6 +7048,8 @@ snapshots:
|
||||
|
||||
etag@1.8.1: {}
|
||||
|
||||
eventemitter3@2.0.3: {}
|
||||
|
||||
execa@5.1.1:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
@@ -6987,6 +7111,10 @@ snapshots:
|
||||
|
||||
exsolve@1.0.8: {}
|
||||
|
||||
extend@3.0.2: {}
|
||||
|
||||
fast-diff@1.1.2: {}
|
||||
|
||||
fast-glob@3.3.3:
|
||||
dependencies:
|
||||
'@nodelib/fs.stat': 2.0.5
|
||||
@@ -7061,6 +7189,8 @@ snapshots:
|
||||
|
||||
function-bind@1.1.2: {}
|
||||
|
||||
functions-have-names@1.2.3: {}
|
||||
|
||||
generic-names@4.0.0:
|
||||
dependencies:
|
||||
loader-utils: 3.3.1
|
||||
@@ -7123,6 +7253,10 @@ snapshots:
|
||||
|
||||
has-flag@4.0.0: {}
|
||||
|
||||
has-property-descriptors@1.0.2:
|
||||
dependencies:
|
||||
es-define-property: 1.0.1
|
||||
|
||||
has-symbols@1.1.0: {}
|
||||
|
||||
has-tostringtag@1.0.2:
|
||||
@@ -7200,6 +7334,11 @@ snapshots:
|
||||
|
||||
ipaddr.js@1.9.1: {}
|
||||
|
||||
is-arguments@1.2.0:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
has-tostringtag: 1.0.2
|
||||
|
||||
is-arrayish@0.2.1: {}
|
||||
|
||||
is-binary-path@2.1.0:
|
||||
@@ -7210,6 +7349,11 @@ snapshots:
|
||||
dependencies:
|
||||
hasown: 2.0.2
|
||||
|
||||
is-date-object@1.1.0:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
has-tostringtag: 1.0.2
|
||||
|
||||
is-extglob@2.1.1: {}
|
||||
|
||||
is-fullwidth-code-point@3.0.0: {}
|
||||
@@ -7226,6 +7370,13 @@ snapshots:
|
||||
|
||||
is-potential-custom-element-name@1.0.1: {}
|
||||
|
||||
is-regex@1.2.1:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
gopd: 1.2.0
|
||||
has-tostringtag: 1.0.2
|
||||
hasown: 2.0.2
|
||||
|
||||
is-stream@2.0.1: {}
|
||||
|
||||
is-typedarray@1.0.0: {}
|
||||
@@ -7868,6 +8019,13 @@ snapshots:
|
||||
|
||||
object-inspect@1.13.4: {}
|
||||
|
||||
object-is@1.1.6:
|
||||
dependencies:
|
||||
call-bind: 1.0.8
|
||||
define-properties: 1.2.1
|
||||
|
||||
object-keys@1.1.1: {}
|
||||
|
||||
ofetch@1.5.1:
|
||||
dependencies:
|
||||
destr: 2.0.5
|
||||
@@ -7906,6 +8064,8 @@ snapshots:
|
||||
|
||||
pako@1.0.11: {}
|
||||
|
||||
parchment@1.1.4: {}
|
||||
|
||||
parse-bmfont-ascii@1.0.6: {}
|
||||
|
||||
parse-bmfont-binary@1.0.6: {}
|
||||
@@ -7986,6 +8146,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
|
||||
pinyin-pro@3.27.0: {}
|
||||
|
||||
pirates@4.0.7: {}
|
||||
|
||||
pixelmatch@4.0.2:
|
||||
@@ -8115,6 +8277,25 @@ snapshots:
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
quill-delta@3.6.3:
|
||||
dependencies:
|
||||
deep-equal: 1.1.2
|
||||
extend: 3.0.2
|
||||
fast-diff: 1.1.2
|
||||
|
||||
quill-image-resize-mp@3.0.1:
|
||||
dependencies:
|
||||
lodash: 4.17.21
|
||||
|
||||
quill@1.3.7:
|
||||
dependencies:
|
||||
clone: 2.1.2
|
||||
deep-equal: 1.1.2
|
||||
eventemitter3: 2.0.3
|
||||
extend: 3.0.2
|
||||
parchment: 1.1.4
|
||||
quill-delta: 3.6.3
|
||||
|
||||
range-parser@1.2.1: {}
|
||||
|
||||
raw-body@2.5.2:
|
||||
@@ -8144,6 +8325,15 @@ snapshots:
|
||||
|
||||
regenerator-runtime@0.14.1: {}
|
||||
|
||||
regexp.prototype.flags@1.5.4:
|
||||
dependencies:
|
||||
call-bind: 1.0.8
|
||||
define-properties: 1.2.1
|
||||
es-errors: 1.3.0
|
||||
get-proto: 1.0.1
|
||||
gopd: 1.2.0
|
||||
set-function-name: 2.0.2
|
||||
|
||||
regexpu-core@6.4.0:
|
||||
dependencies:
|
||||
regenerate: 1.4.2
|
||||
@@ -8266,6 +8456,22 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
set-function-length@1.2.2:
|
||||
dependencies:
|
||||
define-data-property: 1.1.4
|
||||
es-errors: 1.3.0
|
||||
function-bind: 1.1.2
|
||||
get-intrinsic: 1.3.0
|
||||
gopd: 1.2.0
|
||||
has-property-descriptors: 1.0.2
|
||||
|
||||
set-function-name@2.0.2:
|
||||
dependencies:
|
||||
define-data-property: 1.1.4
|
||||
es-errors: 1.3.0
|
||||
functions-have-names: 1.2.3
|
||||
has-property-descriptors: 1.0.2
|
||||
|
||||
setprototypeof@1.2.0: {}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
//设置环境(打包前修改此变量)
|
||||
const ENV = "DEV";
|
||||
const UNI_APP = {};
|
||||
// 表情包路径
|
||||
UNI_APP.EMO_URL = "/static/emoji/";
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序的本地表情包经常莫名失效,建议将表情放到服务器中
|
||||
// UNI_APP.EMO_URL = "https://www.boxim.online/emoji/";
|
||||
// #endif
|
||||
|
||||
if (ENV == "DEV") {
|
||||
UNI_APP.BASE_URL = "http://chatsever.zianxin.com";
|
||||
// UNI_APP.BASE_URL = "https://www.boxim.online/api";
|
||||
UNI_APP.WS_URL = "ws://chatapp.zianxin.com/im";
|
||||
// UNI_APP.WS_URL = "wss://www.boxim.online/im";
|
||||
// H5 走本地代理解决跨域问题
|
||||
// #ifdef H5
|
||||
UNI_APP.BASE_URL = "/api";
|
||||
// #endif
|
||||
}
|
||||
if (ENV == "PROD") {
|
||||
UNI_APP.BASE_URL = "https://www.boxim.online/api";
|
||||
UNI_APP.WS_URL = "wss://www.boxim.online/im";
|
||||
}
|
||||
export default UNI_APP;
|
||||
@@ -1,47 +1,615 @@
|
||||
<script setup>
|
||||
<script>
|
||||
import { onLaunch, onShow, onError } from '@dcloudio/uni-app';
|
||||
import { ShoproInit } from './sheep';
|
||||
import RecentApi from './sheep/api/member/recent';
|
||||
import http from './common/request';
|
||||
import * as msgType from './common/messageType';
|
||||
import * as enums from './common/enums';
|
||||
import * as wsApi from './common/wssocket';
|
||||
import UNI_APP from '@/.env.js';
|
||||
|
||||
onLaunch(() => {
|
||||
// 隐藏原生导航栏 使用自定义底部导航
|
||||
// uni.hideTabBar({
|
||||
// fail: () => {},
|
||||
// });
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isExit: false, // 是否已退出
|
||||
audioTip: null,
|
||||
reconnecting: false, // 正在重连标志
|
||||
privateMessagesBuffer: [],
|
||||
groupMessagesBuffer: [],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
console.log('xxxc');
|
||||
|
||||
// 加载Shopro底层依赖
|
||||
ShoproInit();
|
||||
this.reconnecting = false;
|
||||
this.isExit = false;
|
||||
// 加载数据
|
||||
this.loadStore()
|
||||
.then(() => {
|
||||
// 初始化websocket
|
||||
this.initWebSocket();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
this.exit();
|
||||
});
|
||||
},
|
||||
initWebSocket() {
|
||||
let loginInfo = uni.getStorageSync('loginInfo');
|
||||
wsApi.connect(UNI_APP.WS_URL, loginInfo.accessToken);
|
||||
wsApi.onConnect(() => {
|
||||
if (this.reconnecting) {
|
||||
// 重连成功
|
||||
this.onReconnectWs();
|
||||
} else {
|
||||
// 加载离线消息
|
||||
this.pullOfflineMessage();
|
||||
this.configStore.setAppInit(true);
|
||||
}
|
||||
});
|
||||
wsApi.onMessage((cmd, msgInfo) => {
|
||||
if (cmd == 2) {
|
||||
// 异地登录,强制下线
|
||||
uni.showModal({
|
||||
content: '您已在其他地方登录,将被强制下线',
|
||||
showCancel: false,
|
||||
});
|
||||
this.exit();
|
||||
} else if (cmd == 3) {
|
||||
if (!this.configStore.appInit || this.chatStore.loading) {
|
||||
// 如果正在拉取离线消息,先存入缓存区,等待消息拉取完成再处理,防止消息乱序
|
||||
this.privateMessagesBuffer.push(msgInfo);
|
||||
} else {
|
||||
// 插入私聊消息
|
||||
this.handlePrivateMessage(msgInfo);
|
||||
}
|
||||
} else if (cmd == 4) {
|
||||
if (!this.configStore.appInit || this.chatStore.loading) {
|
||||
// 如果正在拉取离线消息,先存入缓存区,等待消息拉取完成再处理,防止消息乱序
|
||||
this.privateMessagesBuffer.push(msgInfo);
|
||||
} else {
|
||||
// 插入群聊消息
|
||||
this.handleGroupMessage(msgInfo);
|
||||
}
|
||||
} else if (cmd == 5) {
|
||||
// 系统消息
|
||||
this.handleSystemMessage(msgInfo);
|
||||
}
|
||||
});
|
||||
wsApi.onClose((res) => {
|
||||
console.log('ws断开', res);
|
||||
// 重新连接
|
||||
if (!this.reconnecting) {
|
||||
this.reconnectWs();
|
||||
this.configStore.setAppInit(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
loadStore() {
|
||||
console.log(this.userStore);
|
||||
|
||||
// 更新用户位置
|
||||
uni.getLocation({
|
||||
success: (res) => {
|
||||
const params = {
|
||||
issueLat: String(res.latitude),
|
||||
issueLong: String(res.longitude),
|
||||
return this.userStore.loadUser().then(() => {
|
||||
const promises = [];
|
||||
promises.push(this.friendStore.loadFriend());
|
||||
promises.push(this.groupStore.loadGroup());
|
||||
promises.push(this.chatStore.loadChat());
|
||||
promises.push(this.configStore.loadConfig());
|
||||
return Promise.all(promises);
|
||||
});
|
||||
},
|
||||
unloadStore() {
|
||||
this.friendStore.clear();
|
||||
this.groupStore.clear();
|
||||
this.chatStore.clear();
|
||||
this.configStore.clear();
|
||||
this.userStore.clear();
|
||||
},
|
||||
pullOfflineMessage() {
|
||||
this.chatStore.setLoading(true);
|
||||
const promises = [];
|
||||
promises.push(this.pullPrivateOfflineMessage(this.chatStore.privateMsgMaxId));
|
||||
promises.push(this.pullGroupOfflineMessage(this.chatStore.groupMsgMaxId));
|
||||
Promise.all(promises)
|
||||
.then((messages) => {
|
||||
// 处理离线消息
|
||||
messages[0].forEach((m) => this.handlePrivateMessage(m));
|
||||
messages[1].forEach((m) => this.handleGroupMessage(m));
|
||||
// 处理缓冲区收到的实时消息
|
||||
this.privateMessagesBuffer.forEach((m) => this.handlePrivateMessage(m));
|
||||
this.groupMessagesBuffer.forEach((m) => this.handleGroupMessage(m));
|
||||
// 清空缓冲区
|
||||
this.privateMessagesBuffer = [];
|
||||
this.groupMessagesBuffer = [];
|
||||
// 关闭加载离线标记
|
||||
this.chatStore.setLoading(false);
|
||||
// 刷新会话
|
||||
this.chatStore.refreshChats();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
uni.showToast({
|
||||
title: '拉取离线消息失败',
|
||||
icon: 'none',
|
||||
});
|
||||
this.exit();
|
||||
});
|
||||
},
|
||||
pullPrivateOfflineMessage(minId) {
|
||||
return this.$http({
|
||||
url: '/message/private/loadOfflineMessage?minId=' + minId,
|
||||
method: 'GET',
|
||||
timeout: 60000,
|
||||
});
|
||||
},
|
||||
pullGroupOfflineMessage(minId) {
|
||||
return this.$http({
|
||||
url: '/message/group/loadOfflineMessage?minId=' + minId,
|
||||
method: 'GET',
|
||||
timeout: 60000,
|
||||
});
|
||||
},
|
||||
handlePrivateMessage(msg) {
|
||||
// 标记这条消息是不是自己发的
|
||||
msg.selfSend = msg.sendId == this.userStore.userInfo.id;
|
||||
// 好友id
|
||||
let friendId = msg.selfSend ? msg.recvId : msg.sendId;
|
||||
// 会话信息
|
||||
let chatInfo = {
|
||||
type: 'PRIVATE',
|
||||
targetId: friendId,
|
||||
};
|
||||
RecentApi.updateUserLocation(params);
|
||||
// 消息已读处理,清空已读数量
|
||||
if (msg.type == enums.MESSAGE_TYPE.READED) {
|
||||
this.chatStore.resetUnreadCount(chatInfo);
|
||||
return;
|
||||
}
|
||||
// 消息回执处理,改消息状态为已读
|
||||
if (msg.type == enums.MESSAGE_TYPE.RECEIPT) {
|
||||
this.chatStore.readedMessage({
|
||||
friendId: msg.sendId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 消息撤回
|
||||
if (msg.type == enums.MESSAGE_TYPE.RECALL) {
|
||||
this.chatStore.recallMessage(msg, chatInfo);
|
||||
return;
|
||||
}
|
||||
// 新增好友
|
||||
if (msg.type == enums.MESSAGE_TYPE.FRIEND_NEW) {
|
||||
this.friendStore.addFriend(JSON.parse(msg.content));
|
||||
return;
|
||||
}
|
||||
// 删除好友
|
||||
if (msg.type == enums.MESSAGE_TYPE.FRIEND_DEL) {
|
||||
this.friendStore.removeFriend(friendId);
|
||||
return;
|
||||
}
|
||||
// 对好友设置免打扰
|
||||
if (msg.type == enums.MESSAGE_TYPE.FRIEND_DND) {
|
||||
this.friendStore.setDnd(friendId, JSON.parse(msg.content));
|
||||
this.chatStore.setDnd(chatInfo, JSON.parse(msg.content));
|
||||
return;
|
||||
}
|
||||
// 消息插入
|
||||
let friend = this.loadFriendInfo(friendId);
|
||||
this.insertPrivateMessage(friend, msg);
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.showToast({ title: '位置信息获取失败', icon: 'none' });
|
||||
insertPrivateMessage(friend, msg) {
|
||||
// 单人视频信令
|
||||
if (msgType.isRtcPrivate(msg.type)) {
|
||||
// #ifdef MP-WEIXIN
|
||||
// 小程序不支持音视频
|
||||
return;
|
||||
// #endif
|
||||
// 被呼叫,弹出视频页面
|
||||
let delayTime = 100;
|
||||
if (
|
||||
msg.type == enums.MESSAGE_TYPE.RTC_CALL_VOICE ||
|
||||
msg.type == enums.MESSAGE_TYPE.RTC_CALL_VIDEO
|
||||
) {
|
||||
let mode = msg.type == enums.MESSAGE_TYPE.RTC_CALL_VIDEO ? 'video' : 'voice';
|
||||
let pages = getCurrentPages();
|
||||
let curPage = pages[pages.length - 1].route;
|
||||
if (curPage != 'pages/chat/chat-private-video') {
|
||||
const friendInfo = encodeURIComponent(JSON.stringify(friend));
|
||||
uni.navigateTo({
|
||||
url: `/pages/chat/chat-private-video?mode=${mode}&friend=${friendInfo}&isHost=false`,
|
||||
});
|
||||
delayTime = 500;
|
||||
}
|
||||
}
|
||||
setTimeout(() => {
|
||||
uni.$emit('WS_RTC_PRIVATE', msg);
|
||||
}, delayTime);
|
||||
return;
|
||||
}
|
||||
// 插入消息
|
||||
if (msgType.isNormal(msg.type) || msgType.isTip(msg.type) || msgType.isAction(msg.type)) {
|
||||
let chatInfo = {
|
||||
type: 'PRIVATE',
|
||||
targetId: friend.id,
|
||||
showName: friend.nickName,
|
||||
headImage: friend.headImage,
|
||||
isDnd: friend.isDnd,
|
||||
};
|
||||
// 打开会话
|
||||
this.chatStore.openChat(chatInfo);
|
||||
// 插入消息
|
||||
this.chatStore.insertMessage(msg, chatInfo);
|
||||
// 播放提示音
|
||||
if (
|
||||
!friend.isDnd &&
|
||||
!this.chatStore.loading &&
|
||||
!msg.selfSend &&
|
||||
msgType.isNormal(msg.type) &&
|
||||
msg.status != enums.MESSAGE_STATUS.READED
|
||||
) {
|
||||
this.playAudioTip();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
handleGroupMessage(msg) {
|
||||
// 标记这条消息是不是自己发的
|
||||
msg.selfSend = msg.sendId == this.userStore.userInfo.id;
|
||||
let chatInfo = {
|
||||
type: 'GROUP',
|
||||
targetId: msg.groupId,
|
||||
};
|
||||
// 消息已读处理
|
||||
if (msg.type == enums.MESSAGE_TYPE.READED) {
|
||||
// 我已读对方的消息,清空已读数量
|
||||
this.chatStore.resetUnreadCount(chatInfo);
|
||||
return;
|
||||
}
|
||||
// 消息回执处理
|
||||
if (msg.type == enums.MESSAGE_TYPE.RECEIPT) {
|
||||
// 更新消息已读人数
|
||||
let msgInfo = {
|
||||
id: msg.id,
|
||||
groupId: msg.groupId,
|
||||
readedCount: msg.readedCount,
|
||||
receiptOk: msg.receiptOk,
|
||||
};
|
||||
this.chatStore.updateMessage(msgInfo, chatInfo);
|
||||
return;
|
||||
}
|
||||
// 消息撤回
|
||||
if (msg.type == enums.MESSAGE_TYPE.RECALL) {
|
||||
this.chatStore.recallMessage(msg, chatInfo);
|
||||
return;
|
||||
}
|
||||
// 新增群
|
||||
if (msg.type == enums.MESSAGE_TYPE.GROUP_NEW) {
|
||||
this.groupStore.addGroup(JSON.parse(msg.content));
|
||||
return;
|
||||
}
|
||||
// 删除群
|
||||
if (msg.type == enums.MESSAGE_TYPE.GROUP_DEL) {
|
||||
this.groupStore.removeGroup(msg.groupId);
|
||||
return;
|
||||
}
|
||||
// 对群设置免打扰
|
||||
if (msg.type == enums.MESSAGE_TYPE.GROUP_DND) {
|
||||
this.groupStore.setDnd(msg.groupId, JSON.parse(msg.content));
|
||||
this.chatStore.setDnd(chatInfo, JSON.parse(msg.content));
|
||||
return;
|
||||
}
|
||||
// 插入消息
|
||||
let group = this.loadGroupInfo(msg.groupId);
|
||||
this.insertGroupMessage(group, msg);
|
||||
},
|
||||
handleSystemMessage(msg) {
|
||||
if (msg.type == enums.MESSAGE_TYPE.USER_BANNED) {
|
||||
// 用户被封禁
|
||||
wsApi.close(3099);
|
||||
uni.showModal({
|
||||
content: '您的账号已被管理员封禁,原因:' + msg.content,
|
||||
showCancel: false,
|
||||
});
|
||||
this.exit();
|
||||
}
|
||||
},
|
||||
insertGroupMessage(group, msg) {
|
||||
// 群视频信令
|
||||
if (msgType.isRtcGroup(msg.type)) {
|
||||
// #ifdef MP-WEIXIN
|
||||
// 小程序不支持音视频
|
||||
return;
|
||||
// #endif
|
||||
// 被呼叫,弹出视频页面
|
||||
let delayTime = 100;
|
||||
if (msg.type == enums.MESSAGE_TYPE.RTC_GROUP_SETUP) {
|
||||
let pages = getCurrentPages();
|
||||
let curPage = pages[pages.length - 1].route;
|
||||
if (curPage != 'pages/chat/chat-group-video') {
|
||||
const userInfos = encodeURIComponent(msg.content);
|
||||
const inviterId = msg.sendId;
|
||||
const groupId = msg.groupId;
|
||||
uni.navigateTo({
|
||||
url: `/pages/chat/chat-group-video?groupId=${groupId}&isHost=false
|
||||
&inviterId=${inviterId}&userInfos=${userInfos}`,
|
||||
});
|
||||
delayTime = 500;
|
||||
}
|
||||
}
|
||||
// 消息转发到chat-group-video页面进行处理
|
||||
setTimeout(() => {
|
||||
uni.$emit('WS_RTC_GROUP', msg);
|
||||
}, delayTime);
|
||||
return;
|
||||
}
|
||||
// 插入消息
|
||||
if (msgType.isNormal(msg.type) || msgType.isTip(msg.type) || msgType.isAction(msg.type)) {
|
||||
let chatInfo = {
|
||||
type: 'GROUP',
|
||||
targetId: group.id,
|
||||
showName: group.showGroupName,
|
||||
headImage: group.headImageThumb,
|
||||
isDnd: group.isDnd,
|
||||
};
|
||||
// 打开会话
|
||||
this.chatStore.openChat(chatInfo);
|
||||
// 插入消息
|
||||
this.chatStore.insertMessage(msg, chatInfo);
|
||||
// 播放提示音
|
||||
if (
|
||||
!group.isDnd &&
|
||||
!this.chatStore.loading &&
|
||||
!msg.selfSend &&
|
||||
msgType.isNormal(msg.type) &&
|
||||
msg.status != enums.MESSAGE_STATUS.READED
|
||||
) {
|
||||
this.playAudioTip();
|
||||
}
|
||||
}
|
||||
},
|
||||
loadFriendInfo(id, callback) {
|
||||
let friend = this.friendStore.findFriend(id);
|
||||
if (!friend) {
|
||||
console.log('未知用户:', id);
|
||||
friend = {
|
||||
id: id,
|
||||
showNickName: '未知用户',
|
||||
headImage: '',
|
||||
};
|
||||
}
|
||||
return friend;
|
||||
},
|
||||
loadGroupInfo(id) {
|
||||
let group = this.groupStore.findGroup(id);
|
||||
if (!group) {
|
||||
group = {
|
||||
id: id,
|
||||
showGroupName: '未知群聊',
|
||||
headImageThumb: '',
|
||||
};
|
||||
}
|
||||
return group;
|
||||
},
|
||||
exit() {
|
||||
console.log('exit');
|
||||
this.isExit = true;
|
||||
wsApi.close(3099);
|
||||
uni.removeStorageSync('loginInfo');
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/login',
|
||||
});
|
||||
this.unloadStore();
|
||||
},
|
||||
playAudioTip() {
|
||||
// 音频播放无法成功
|
||||
// this.audioTip = uni.createInnerAudioContext();
|
||||
// this.audioTip.src = "/static/audio/tip.wav";
|
||||
// this.audioTip.play();
|
||||
},
|
||||
refreshToken(loginInfo) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!loginInfo || !loginInfo.refreshToken) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
http({
|
||||
url: '/refreshToken',
|
||||
method: 'PUT',
|
||||
header: {
|
||||
refreshToken: loginInfo.refreshToken,
|
||||
},
|
||||
})
|
||||
.then((newLoginInfo) => {
|
||||
uni.setStorageSync('loginInfo', newLoginInfo);
|
||||
resolve();
|
||||
})
|
||||
.catch((e) => {
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
},
|
||||
reconnectWs() {
|
||||
// 已退出则不再重连
|
||||
if (this.isExit) {
|
||||
return;
|
||||
}
|
||||
// 记录标志
|
||||
this.reconnecting = true;
|
||||
// 重新加载一次个人信息,目的是为了保证网络已经正常且token有效
|
||||
this.userStore
|
||||
.loadUser()
|
||||
.then((userInfo) => {
|
||||
uni.showToast({
|
||||
title: '连接已断开,尝试重新连接...',
|
||||
icon: 'none',
|
||||
});
|
||||
// 重新连接
|
||||
let loginInfo = uni.getStorageSync('loginInfo');
|
||||
wsApi.reconnect(UNI_APP.WS_URL, loginInfo.accessToken);
|
||||
})
|
||||
.catch(() => {
|
||||
// 5s后重试
|
||||
setTimeout(() => {
|
||||
this.reconnectWs();
|
||||
}, 5000);
|
||||
});
|
||||
},
|
||||
onReconnectWs() {
|
||||
this.reconnecting = false;
|
||||
// 重新加载好友和群聊
|
||||
const promises = [];
|
||||
promises.push(this.friendStore.loadFriend());
|
||||
promises.push(this.groupStore.loadGroup());
|
||||
Promise.all(promises)
|
||||
.then(() => {
|
||||
uni.showToast({
|
||||
title: '已重新连接',
|
||||
icon: 'none',
|
||||
});
|
||||
// 加载离线消息
|
||||
this.pullPrivateOfflineMessage(this.chatStore.privateMsgMaxId);
|
||||
this.pullGroupOfflineMessage(this.chatStore.groupMsgMaxId);
|
||||
this.configStore.setAppInit(true);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
this.exit();
|
||||
});
|
||||
},
|
||||
closeSplashscreen(delay) {
|
||||
// #ifdef APP-PLUS
|
||||
// 关闭开机动画
|
||||
setTimeout(() => {
|
||||
plus.navigator.closeSplashscreen();
|
||||
}, delay);
|
||||
// #endif
|
||||
},
|
||||
},
|
||||
onLaunch() {
|
||||
// 隐藏原生导航栏 使用自定义底部导航
|
||||
// uni.hideTabBar({
|
||||
// fail: () => {},
|
||||
// });
|
||||
// 加载Shopro底层依赖
|
||||
ShoproInit();
|
||||
// 更新用户位置
|
||||
uni.getLocation({
|
||||
success: (res) => {
|
||||
const params = {
|
||||
issueLat: String(res.latitude),
|
||||
issueLong: String(res.longitude),
|
||||
};
|
||||
RecentApi.updateUserLocation(params);
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.showToast({ title: '位置信息获取失败', icon: 'none' });
|
||||
},
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
// #ifdef APP-PLUS
|
||||
// 获取urlSchemes参数
|
||||
const args = plus.runtime.arguments;
|
||||
if (args) {
|
||||
}
|
||||
this.$mountStore();
|
||||
// 延迟1s,避免用户看到页面跳转
|
||||
this.closeSplashscreen(1000);
|
||||
uni.setStorageSync('loginInfo', {
|
||||
accessToken:
|
||||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIyIiwiZXhwIjoxNzY0MDU5ODk2LCJpbmZvIjoie1wibmlja05hbWVcIjpcInd3d1wiLFwidGVybWluYWxcIjoxLFwidXNlcklkXCI6MixcInVzZXJOYW1lXCI6XCJ3d3dcIn0ifQ.xBfQJg6FFHQGzkK_IUGZUjVN8g5tex3VtWsTr1gGjzg',
|
||||
accessTokenExpiresIn: 1800,
|
||||
refreshToken:
|
||||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIyIiwiZXhwIjoxNzY0NjYyODk2LCJpbmZvIjoie1wibmlja05hbWVcIjpcInd3d1wiLFwidGVybWluYWxcIjoxLFwidXNlcklkXCI6MixcInVzZXJOYW1lXCI6XCJ3d3dcIn0ifQ.o87SSm-pWBkPKROeNBP45VmpBVSdw__Kps4YfiJPZwg',
|
||||
refreshTokenExpiresIn: 604800,
|
||||
});
|
||||
// 登录状态校验
|
||||
let loginInfo = uni.getStorageSync('loginInfo');
|
||||
this.refreshToken(loginInfo)
|
||||
.then(() => {
|
||||
// #ifdef H5
|
||||
// 跳转到聊天页
|
||||
uni.switchTab({
|
||||
url: '/pages/message/message',
|
||||
});
|
||||
// #endif
|
||||
// 初始化
|
||||
this.init();
|
||||
this.closeSplashscreen(0);
|
||||
})
|
||||
.catch(() => {
|
||||
// 跳转到登录页
|
||||
// uni.navigateTo({
|
||||
// url: '/pages/login/login',
|
||||
// });
|
||||
});
|
||||
},
|
||||
onShow() {
|
||||
// #ifdef APP-PLUS
|
||||
// 获取urlSchemes参数
|
||||
const args = plus.runtime.arguments;
|
||||
if (args) {
|
||||
}
|
||||
|
||||
// 获取剪贴板
|
||||
uni.getClipboardData({
|
||||
success: (res) => {},
|
||||
});
|
||||
// #endif
|
||||
});
|
||||
// 获取剪贴板
|
||||
uni.getClipboardData({
|
||||
success: (res) => {},
|
||||
});
|
||||
// #endif
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* @import '@/sheep/scss/index.scss'; */
|
||||
@import '@/im.scss';
|
||||
@import url('./static/icon/iconfont.css');
|
||||
// #ifdef H5
|
||||
uni-page-head {
|
||||
display: none; // h5浏览器本身就有标题
|
||||
}
|
||||
// #endif
|
||||
.tab-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// #ifdef H5
|
||||
height: calc(100vh - 50px - $im-nav-bar-height); // h5平台100vh是包含了底部高度,需要减去
|
||||
top: $im-nav-bar-height;
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
height: calc(
|
||||
100vh - var(--status-bar-height) - $im-nav-bar-height
|
||||
); // app平台还要减去顶部手机状态栏高度
|
||||
top: calc($im-nav-bar-height + var(--status-bar-height));
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
height: calc(100vh - $im-nav-bar-height);
|
||||
top: $im-nav-bar-height;
|
||||
|
||||
// #endif
|
||||
color: $im-text-color;
|
||||
background: $im-bg-linear;
|
||||
font-size: $im-font-size;
|
||||
font-family: $font-family;
|
||||
}
|
||||
|
||||
.page {
|
||||
position: relative;
|
||||
// #ifdef H5
|
||||
height: calc(100vh - $im-nav-bar-height); // h5平台100vh是包含了底部高度,需要减去
|
||||
top: $im-nav-bar-height;
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
height: calc(
|
||||
100vh - var(--status-bar-height) - $im-nav-bar-height
|
||||
); // app平台还要减去顶部手机状态栏高度
|
||||
top: calc($im-nav-bar-height + var(--status-bar-height));
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
height: calc(100vh - $im-nav-bar-height);
|
||||
top: $im-nav-bar-height;
|
||||
// #endif
|
||||
|
||||
color: $im-text-color;
|
||||
background: $im-bg-linear;
|
||||
font-size: $im-font-size;
|
||||
font-family: $font-family;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,66 @@
|
||||
let toTimeText = (timeStamp, simple) => {
|
||||
var dateTime = new Date(timeStamp)
|
||||
var currentTime = Date.parse(new Date()); //当前时间
|
||||
var timeDiff = currentTime - dateTime; //与当前时间误差
|
||||
var timeText = '';
|
||||
if (timeDiff <= 60000) { //一分钟内
|
||||
timeText = '刚刚';
|
||||
} else if (timeDiff > 60000 && timeDiff < 3600000) {
|
||||
//1小时内
|
||||
timeText = Math.floor(timeDiff / 60000) + '分钟前';
|
||||
} else if (timeDiff >= 3600000 && timeDiff < 86400000 && !isYestday(dateTime)) {
|
||||
//今日
|
||||
timeText = formatDateTime(dateTime).substr(11, 5);
|
||||
} else if (isYestday(dateTime)) {
|
||||
//昨天
|
||||
timeText = '昨天' + formatDateTime(dateTime).substr(11, 5);
|
||||
} else if (isYear(dateTime)) {
|
||||
//今年
|
||||
timeText = formatDateTime(dateTime).substr(5, simple ? 5 : 14);
|
||||
} else {
|
||||
//不属于今年
|
||||
timeText = formatDateTime(dateTime);
|
||||
if (simple) {
|
||||
timeText = timeText.substr(2, 8);
|
||||
}
|
||||
}
|
||||
return timeText;
|
||||
}
|
||||
|
||||
let isYestday = (date) => {
|
||||
var yesterday = new Date(new Date() - 1000 * 60 * 60 * 24);
|
||||
return yesterday.getYear() === date.getYear() &&
|
||||
yesterday.getMonth() === date.getMonth() &&
|
||||
yesterday.getDate() === date.getDate();
|
||||
}
|
||||
|
||||
let isYear = (date) => {
|
||||
return date.getYear() === new Date().getYear();
|
||||
}
|
||||
|
||||
let formatDateTime = (date) => {
|
||||
if (date === '' || !date) {
|
||||
return ''
|
||||
}
|
||||
var dateObject = new Date(date)
|
||||
var y = dateObject.getFullYear()
|
||||
var m = dateObject.getMonth() + 1
|
||||
m = m < 10 ? ('0' + m) : m
|
||||
var d = dateObject.getDate()
|
||||
d = d < 10 ? ('0' + d) : d
|
||||
var h = dateObject.getHours()
|
||||
h = h < 10 ? ('0' + h) : h
|
||||
var minute = dateObject.getMinutes()
|
||||
minute = minute < 10 ? ('0' + minute) : minute
|
||||
var second = dateObject.getSeconds()
|
||||
second = second < 10 ? ('0' + second) : second
|
||||
return y + '/' + m + '/' + d + ' ' + h + ':' + minute + ':' + second
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
toTimeText,
|
||||
isYestday,
|
||||
isYear,
|
||||
formatDateTime
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import UNI_APP from '@/.env.js'
|
||||
|
||||
|
||||
const emoTextList = ['憨笑', '媚眼', '开心', '坏笑', '可怜', '爱心', '笑哭', '拍手', '惊喜', '打气',
|
||||
'大哭', '流泪', '饥饿', '难受', '健身', '示爱', '色色', '眨眼', '暴怒', '惊恐',
|
||||
'思考', '头晕', '大吐', '酷笑', '翻滚', '享受', '鼻涕', '快乐', '雀跃', '微笑',
|
||||
'贪婪', '红心', '粉心', '星星', '大火', '眼睛', '音符', "叹号", "问号", "绿叶",
|
||||
"燃烧", "喇叭", "警告", "信封", "房子", "礼物", "点赞", "举手", "拍手", "点头",
|
||||
"摇头", "偷瞄", "庆祝", "疾跑", "打滚", "惊吓", "起跳"
|
||||
];
|
||||
|
||||
const regex = /\#[\u4E00-\u9FA5]{1,3}\;/gi;
|
||||
|
||||
let containEmoji = (content) => {
|
||||
return regex.test(content)
|
||||
}
|
||||
|
||||
let transform = (content, extClass) => {
|
||||
return content.replace(regex, (emoText)=>{
|
||||
// 将匹配结果替换表情图片
|
||||
let word = emoText.replace(/\#|\;/gi, '');
|
||||
let idx = emoTextList.indexOf(word);
|
||||
if (idx == -1) {
|
||||
return emoText;
|
||||
}
|
||||
let path = textToPath(emoText);
|
||||
let img = `<img src="${path}" class="${extClass}"/>`;
|
||||
return img;
|
||||
});
|
||||
}
|
||||
|
||||
let textToPath = (emoText) => {
|
||||
let word = emoText.replace(/\#|\;/gi, '');
|
||||
let idx = emoTextList.indexOf(word);
|
||||
return UNI_APP.EMO_URL + idx + ".gif";
|
||||
}
|
||||
|
||||
export default {
|
||||
containEmoji,
|
||||
emoTextList,
|
||||
transform,
|
||||
textToPath
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
|
||||
const MESSAGE_TYPE = {
|
||||
TEXT: 0,
|
||||
IMAGE: 1,
|
||||
FILE: 2,
|
||||
AUDIO: 3,
|
||||
VIDEO: 4,
|
||||
RECALL: 10,
|
||||
READED: 11,
|
||||
RECEIPT: 12,
|
||||
TIP_TIME: 20,
|
||||
TIP_TEXT: 21,
|
||||
LOADING: 30,
|
||||
ACT_RT_VOICE: 40,
|
||||
ACT_RT_VIDEO: 41,
|
||||
USER_BANNED: 50,
|
||||
FRIEND_NEW: 80,
|
||||
FRIEND_DEL: 81,
|
||||
FRIEND_DND: 82,
|
||||
GROUP_NEW: 90,
|
||||
GROUP_DEL: 91,
|
||||
GROUP_DND: 92,
|
||||
RTC_CALL_VOICE: 100,
|
||||
RTC_CALL_VIDEO: 101,
|
||||
RTC_ACCEPT: 102,
|
||||
RTC_REJECT: 103,
|
||||
RTC_CANCEL: 104,
|
||||
RTC_FAILED: 105,
|
||||
RTC_HANDUP: 106,
|
||||
RTC_CANDIDATE: 107,
|
||||
RTC_GROUP_SETUP: 200,
|
||||
RTC_GROUP_ACCEPT: 201,
|
||||
RTC_GROUP_REJECT: 202,
|
||||
RTC_GROUP_FAILED: 203,
|
||||
RTC_GROUP_CANCEL: 204,
|
||||
RTC_GROUP_QUIT: 205,
|
||||
RTC_GROUP_INVITE: 206,
|
||||
RTC_GROUP_JOIN: 207,
|
||||
RTC_GROUP_OFFER: 208,
|
||||
RTC_GROUP_ANSWER: 209,
|
||||
RTC_GROUP_CANDIDATE: 210,
|
||||
RTC_GROUP_DEVICE: 211
|
||||
}
|
||||
|
||||
const USER_STATE = {
|
||||
OFFLINE: 0,
|
||||
FREE: 1,
|
||||
BUSY: 2
|
||||
}
|
||||
|
||||
const TERMINAL_TYPE = {
|
||||
WEB: 0,
|
||||
APP: 1
|
||||
}
|
||||
|
||||
const MESSAGE_STATUS = {
|
||||
FAILED: -2, // 发送失败
|
||||
SENDING: -1, // 发送中(消息没到服务器)
|
||||
PENDING: 0, // 未送达(消息已到服务器,但对方没收到)
|
||||
DELIVERED: 1, // 已送达(对方已收到,但是未读消息)
|
||||
RECALL: 2, // 已撤回
|
||||
READED: 3, // 消息已读
|
||||
}
|
||||
|
||||
export {
|
||||
MESSAGE_TYPE,
|
||||
USER_STATE,
|
||||
TERMINAL_TYPE,
|
||||
MESSAGE_STATUS
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
|
||||
// 是否普通消息
|
||||
let isNormal = function (type) {
|
||||
return type >= 0 && type < 10;
|
||||
}
|
||||
|
||||
// 是否状态消息
|
||||
let isStatus = function (type) {
|
||||
return type >= 10 && type < 20;
|
||||
}
|
||||
|
||||
// 是否提示消息
|
||||
let isTip = function (type) {
|
||||
return type >= 20 && type < 30;
|
||||
}
|
||||
|
||||
// 操作交互类消息
|
||||
let isAction = function (type) {
|
||||
return type >= 40 && type < 50;
|
||||
}
|
||||
|
||||
// 单人通话信令
|
||||
let isRtcPrivate = function (type) {
|
||||
return type >= 100 && type < 200;
|
||||
}
|
||||
|
||||
// 多人通话信令
|
||||
let isRtcGroup = function (type) {
|
||||
return type >= 200 && type < 300;
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
isNormal,
|
||||
isStatus,
|
||||
isTip,
|
||||
isAction,
|
||||
isRtcPrivate,
|
||||
isRtcGroup
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import UNI_APP from '@/.env.js';
|
||||
|
||||
const rc = uni.getRecorderManager();
|
||||
// 录音开始时间
|
||||
let startTime = null;
|
||||
let checkIsEnable = ()=>{
|
||||
return true;
|
||||
}
|
||||
|
||||
let start = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
rc.onStart(() => {
|
||||
startTime = new Date();
|
||||
resolve()
|
||||
});
|
||||
rc.onError((e) => {
|
||||
console.log(e);
|
||||
reject(e)
|
||||
})
|
||||
rc.start({
|
||||
format: 'mp3' // 录音格式,可选值:aac/mp3
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
let close = () => {
|
||||
rc.stop();
|
||||
}
|
||||
|
||||
let upload = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
rc.onStop((wavFile, a, b) => {
|
||||
uni.uploadFile({
|
||||
url: UNI_APP.BASE_URL + '/file/upload',
|
||||
header: {
|
||||
accessToken: uni.getStorageSync("loginInfo").accessToken
|
||||
},
|
||||
filePath: wavFile.tempFilePath,
|
||||
name: 'file',
|
||||
success: (res) => {
|
||||
let r = JSON.parse(res.data);
|
||||
if (r.code != 200) {
|
||||
reject(r.message);
|
||||
} else {
|
||||
const duration = (new Date().getTime() - startTime.getTime()) /
|
||||
1000
|
||||
const data = {
|
||||
duration: Math.round(duration),
|
||||
url: r.data
|
||||
}
|
||||
resolve(data);
|
||||
}
|
||||
},
|
||||
fail: (e) => {
|
||||
reject(e);
|
||||
}
|
||||
})
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
export {
|
||||
checkIsEnable,
|
||||
start,
|
||||
close,
|
||||
upload
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import UNI_APP from '@/.env.js';
|
||||
|
||||
let rc = null;
|
||||
let duration = 0;
|
||||
let chunks = [];
|
||||
let stream = null;
|
||||
let startTime = null;
|
||||
let checkIsEnable = () => {
|
||||
if (origin.indexOf('https') === -1 && origin.indexOf('localhost') === -1 &&
|
||||
origin.indexOf('127.0.0.1') === -1) {
|
||||
uni.showToast({
|
||||
title: '请在https环境中使用录音功能',
|
||||
icon: 'error'
|
||||
})
|
||||
return false;
|
||||
}
|
||||
if (!navigator.mediaDevices || !window.MediaRecorder) {
|
||||
uni.showToast({
|
||||
title: '当前浏览器不支持录音',
|
||||
icon: 'error'
|
||||
})
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
let start = () => {
|
||||
return navigator.mediaDevices.getUserMedia({ audio: true }).then(audioStream => {
|
||||
startTime = new Date().getTime();
|
||||
chunks = [];
|
||||
stream = audioStream;
|
||||
rc = new MediaRecorder(stream)
|
||||
rc.start()
|
||||
})
|
||||
}
|
||||
|
||||
let close = () => {
|
||||
stream.getTracks().forEach((track) => {
|
||||
track.stop()
|
||||
})
|
||||
rc.stop()
|
||||
}
|
||||
|
||||
|
||||
let upload = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
rc.ondataavailable = (e) => {
|
||||
chunks.push(e.data)
|
||||
}
|
||||
rc.onstop = () => {
|
||||
if(!chunks[0].size){
|
||||
chunks = [];
|
||||
return;
|
||||
}
|
||||
duration = (new Date().getTime() - startTime) / 1000;
|
||||
const newbolb = new Blob(chunks, { 'type': 'audio/mpeg' });
|
||||
const name = new Date().getDate() + '.mp3';
|
||||
const file = new File([newbolb], name)
|
||||
uni.uploadFile({
|
||||
url: UNI_APP.BASE_URL + '/file/upload',
|
||||
header: {
|
||||
accessToken: uni.getStorageSync("loginInfo").accessToken
|
||||
},
|
||||
file: file,
|
||||
name: 'file',
|
||||
success: (res) => {
|
||||
let r = JSON.parse(res.data);
|
||||
if (r.code != 200) {
|
||||
console.log(res)
|
||||
reject(r.message);
|
||||
} else {
|
||||
const data = {
|
||||
duration: parseInt(duration),
|
||||
url: r.data
|
||||
}
|
||||
resolve(data);
|
||||
}
|
||||
},
|
||||
fail: (e) => {
|
||||
reject(e);
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export {
|
||||
checkIsEnable,
|
||||
start,
|
||||
close,
|
||||
upload
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import UNI_APP from '@/.env.js';
|
||||
|
||||
// 请求队列
|
||||
let requestList = [];
|
||||
// 是否正在刷新中
|
||||
let isRefreshToken = false;
|
||||
|
||||
const request = (options) => {
|
||||
const header = options.header || {};
|
||||
const loginInfo = uni.getStorageSync('loginInfo');
|
||||
if (loginInfo) {
|
||||
header.accessToken = loginInfo.accessToken;
|
||||
}
|
||||
return new Promise(function (resolve, reject) {
|
||||
uni.request({
|
||||
url: UNI_APP.BASE_URL + options.url,
|
||||
// url: UNI_APP.BASE_URL + options.url,
|
||||
method: options.method || 'GET',
|
||||
header: header,
|
||||
data: options.data || {},
|
||||
timeout: options.timeout || 3000,
|
||||
async success(res) {
|
||||
if (res.data.code == 200) {
|
||||
return resolve(res.data.data);
|
||||
} else if (res.data.code == 400) {
|
||||
getApp().$vm.exit();
|
||||
} else if (res.data.code == 401) {
|
||||
console.log('token失效,尝试重新获取');
|
||||
if (isRefreshToken) {
|
||||
// 正在刷新token,把其他请求存起来
|
||||
requestList.push(() => {
|
||||
resolve(request(options));
|
||||
});
|
||||
return;
|
||||
}
|
||||
isRefreshToken = true;
|
||||
// 发送请求, 进行刷新token操作, 获取新的token
|
||||
const res = await reqRefreshToken(loginInfo);
|
||||
if (!res || res.data.code != 200) {
|
||||
requestList = [];
|
||||
isRefreshToken = false;
|
||||
console.log('刷新token失败');
|
||||
getApp().$vm.exit();
|
||||
return;
|
||||
}
|
||||
let newInfo = res.data.data;
|
||||
uni.setStorageSync('loginInfo', newInfo);
|
||||
requestList.forEach((cb) => cb());
|
||||
requestList = [];
|
||||
isRefreshToken = false;
|
||||
// 重新发送刚才的请求
|
||||
return resolve(request(options));
|
||||
} else {
|
||||
uni.showToast({
|
||||
icon: 'none',
|
||||
title: res.data.message,
|
||||
duration: 1500,
|
||||
});
|
||||
return reject(res.data);
|
||||
}
|
||||
},
|
||||
fail(error) {
|
||||
uni.showToast({
|
||||
title: '网络似乎有点不给力哟',
|
||||
icon: 'none',
|
||||
duration: 1500,
|
||||
});
|
||||
return reject(error);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const reqRefreshToken = (loginInfo) => {
|
||||
return new Promise(function (resolve, reject) {
|
||||
uni.request({
|
||||
method: 'PUT',
|
||||
url: UNI_APP.BASE_URL + '/refreshToken',
|
||||
header: {
|
||||
refreshToken: loginInfo.refreshToken,
|
||||
},
|
||||
success: (res) => {
|
||||
resolve(res);
|
||||
},
|
||||
fail: (res) => {
|
||||
reject(res);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default request;
|
||||
@@ -0,0 +1,14 @@
|
||||
let html2Escape = (strText) => {
|
||||
return strText.replace(/[<>&"]/g, function(c) {
|
||||
return {
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'&': '&',
|
||||
'"': '"'
|
||||
} [c];
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
html2Escape
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
|
||||
// 使用正则表达式匹配更广泛的URL格式(此正则由deepseek生成)
|
||||
const regex = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|]|\bwww\.[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
|
||||
|
||||
let containUrl = (content) => {
|
||||
return regex.test(content)
|
||||
}
|
||||
|
||||
|
||||
let replaceURLWithHTMLLinks = (content, color) => {
|
||||
return content.replace(regex, (url) => {
|
||||
// 如果URL不以http(s)://开头,则添加http://前缀
|
||||
if (!url.startsWith("http")) {
|
||||
url = "http://" + url;
|
||||
}
|
||||
return `<a href="${url}" target="_blank" style="color: ${color};text-decoration: underline;">${url}</a>`;
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
containUrl,
|
||||
replaceURLWithHTMLLinks
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
let accessToken = "";
|
||||
let messageCallBack = null;
|
||||
let closeCallBack = null;
|
||||
let connectCallBack = null;
|
||||
let isConnect = false; //连接标识 避免重复连接
|
||||
let rec = null;
|
||||
let lastConnectTime = new Date(); // 最后一次连接时间
|
||||
let socketTask = null;
|
||||
|
||||
let connect = (wsurl, token) => {
|
||||
accessToken = token;
|
||||
if (isConnect) {
|
||||
return;
|
||||
}
|
||||
lastConnectTime = new Date();
|
||||
socketTask = uni.connectSocket({
|
||||
url: wsurl,
|
||||
success: (res) => {},
|
||||
fail: (e) => {
|
||||
console.log(e);
|
||||
console.log("WebSocket连接失败,10s后重连");
|
||||
setTimeout(() => {
|
||||
connect();
|
||||
}, 10000)
|
||||
}
|
||||
});
|
||||
|
||||
socketTask.onOpen((res) => {
|
||||
console.log("WebSocket连接成功");
|
||||
isConnect = true;
|
||||
// 发送登录命令
|
||||
let loginInfo = {
|
||||
cmd: 0,
|
||||
data: {
|
||||
accessToken: accessToken
|
||||
}
|
||||
};
|
||||
socketTask.send({
|
||||
data: JSON.stringify(loginInfo)
|
||||
});
|
||||
})
|
||||
|
||||
socketTask.onMessage((res) => {
|
||||
let sendInfo = JSON.parse(res.data)
|
||||
if (sendInfo.cmd == 0) {
|
||||
heartCheck.start()
|
||||
connectCallBack && connectCallBack();
|
||||
console.log('WebSocket登录成功')
|
||||
} else if (sendInfo.cmd == 1) {
|
||||
// 重新开启心跳定时
|
||||
heartCheck.reset();
|
||||
} else {
|
||||
// 其他消息转发出去
|
||||
console.log("接收到消息", sendInfo);
|
||||
messageCallBack && messageCallBack(sendInfo.cmd, sendInfo.data)
|
||||
}
|
||||
})
|
||||
|
||||
socketTask.onClose((res) => {
|
||||
console.log('WebSocket连接关闭')
|
||||
isConnect = false;
|
||||
closeCallBack && closeCallBack(res);
|
||||
})
|
||||
|
||||
socketTask.onError(e => {
|
||||
console.log("ws错误:", e)
|
||||
close(1006);
|
||||
isConnect = false;
|
||||
closeCallBack && closeCallBack(e);
|
||||
})
|
||||
}
|
||||
|
||||
//定义重连函数
|
||||
let reconnect = (wsurl, accessToken) => {
|
||||
console.log("尝试重新连接");
|
||||
if (isConnect) {
|
||||
return;
|
||||
}
|
||||
// 延迟10秒重连 避免过多次过频繁请求重连
|
||||
let timeDiff = new Date().getTime() - lastConnectTime.getTime()
|
||||
let delay = timeDiff < 10000 ? 10000 - timeDiff : 0;
|
||||
rec && clearTimeout(rec);
|
||||
rec = setTimeout(function() {
|
||||
connect(wsurl, accessToken);
|
||||
}, delay);
|
||||
};
|
||||
|
||||
//设置关闭连接
|
||||
let close = (code) => {
|
||||
if (!isConnect) {
|
||||
return;
|
||||
}
|
||||
socketTask.close({
|
||||
code: code,
|
||||
complete: (res) => {
|
||||
isConnect = false;
|
||||
console.log("关闭websocket连接");
|
||||
},
|
||||
fail: (e) => {
|
||||
console.log("关闭websocket连接失败", e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// 心跳设置
|
||||
let heartCheck = {
|
||||
timeout: 20000, //每段时间发送一次心跳包 这里设置为20s
|
||||
timeoutObj: null, //延时发送消息对象(启动心跳新建这个对象,收到消息后重置对象)
|
||||
start: function() {
|
||||
if (isConnect) {
|
||||
console.log('发送WebSocket心跳')
|
||||
let heartBeat = {
|
||||
cmd: 1,
|
||||
data: {}
|
||||
};
|
||||
sendMessage(JSON.stringify(heartBeat))
|
||||
}
|
||||
},
|
||||
reset: function() {
|
||||
clearTimeout(this.timeoutObj);
|
||||
this.timeoutObj = setTimeout(() => heartCheck.start(), this.timeout);
|
||||
}
|
||||
};
|
||||
|
||||
let sendMessage = (message) => {
|
||||
socketTask.send({ data: message })
|
||||
}
|
||||
|
||||
let onConnect = (callback) => {
|
||||
connectCallBack = callback;
|
||||
}
|
||||
|
||||
|
||||
let onMessage = (callback) => {
|
||||
messageCallBack = callback;
|
||||
}
|
||||
|
||||
|
||||
let onClose = (callback) => {
|
||||
closeCallBack = callback;
|
||||
}
|
||||
|
||||
|
||||
// 将方法暴露出去
|
||||
export {
|
||||
connect,
|
||||
reconnect,
|
||||
close,
|
||||
sendMessage,
|
||||
onConnect,
|
||||
onMessage,
|
||||
onClose
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<view class="arrow-bar">
|
||||
<text class="icon iconfont" :class="icon" :style="{color: textColor}"></text>
|
||||
<text class="title">{{ title }}</text>
|
||||
<uni-icons class="arrow" type="right" size="16"></uni-icons>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "arrow-bar",
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
colors: ["#5daa31", "#c7515a", "#e03697", "#85029b",
|
||||
"#c9b455", "#326eb6"]
|
||||
}
|
||||
},
|
||||
computed:{
|
||||
textColor() {
|
||||
let hash = 0;
|
||||
for (var i = 0; i < this.title.length; i++) {
|
||||
hash += this.title.charCodeAt(i);
|
||||
}
|
||||
return this.colors[hash % this.colors.length];
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.arrow-bar {
|
||||
width: 100%;
|
||||
height: 90rpx;
|
||||
font-size: $im-font-size;
|
||||
color: $im-text-color;
|
||||
margin-top: 5rpx;
|
||||
background-color: white;
|
||||
line-height: 90rpx;
|
||||
display: flex;
|
||||
|
||||
.icon {
|
||||
margin-left: 40rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
margin-right: 40rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<view class="bar-group">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "bar-group"
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bar-group {
|
||||
margin: 20rpx 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<view class="btn-bar" :style="style">
|
||||
<text v-if="icon" class="icon iconfont" :class="icon"></text>
|
||||
<text class="title">{{ title }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "btn-bar",
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "normal"
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "#000"
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
style() {
|
||||
let color = "#000";
|
||||
switch (this.type) {
|
||||
case 'danger':
|
||||
color = "#f14747";
|
||||
break;
|
||||
case 'primary':
|
||||
color = "#35567f";
|
||||
break;
|
||||
}
|
||||
return `color: ${color};`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.btn-bar {
|
||||
width: 100%;
|
||||
height: 100rpx;
|
||||
margin-top: 5rpx;
|
||||
background-color: white;
|
||||
line-height: 100rpx;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.icon {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<view class="switch-bar">
|
||||
<text class="title">{{ title }}</text>
|
||||
<switch class="switch" :checked="checked" color="#18bc37" @change="onChange"></switch>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "switch-bar",
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
checked: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
value: this.checked
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onChange(e) {
|
||||
this.value = true;
|
||||
setTimeout(() => {
|
||||
this.value = false;
|
||||
}, 100)
|
||||
//this.value = false;
|
||||
|
||||
this.$emit('change', e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.switch-bar {
|
||||
width: 100%;
|
||||
height: 100rpx;
|
||||
font-size: 34rpx;
|
||||
color: black;
|
||||
margin-top: 5rpx;
|
||||
background-color: white;
|
||||
line-height: 100rpx;
|
||||
display: flex;
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
margin-left: 40rpx;
|
||||
}
|
||||
|
||||
.switch {
|
||||
margin-right: 40rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<uni-popup ref="popup" type="bottom" @change="onChange">
|
||||
<view class="chat-at-box">
|
||||
<view class="chat-at-top">
|
||||
<view class="chat-at-tip"> 选择要提醒的人</view>
|
||||
<button class="chat-at-btn" type="warn" size="mini" @click="onClean()">清空 </button>
|
||||
<button class="chat-at-btn" type="primary" size="mini" @click="onOk()">确定({{ atUserIds.length }})
|
||||
</button>
|
||||
</view>
|
||||
<scroll-view v-show="atUserIds.length > 0" scroll-x="true" scroll-left="120">
|
||||
<view class="at-user-items">
|
||||
<view v-for="m in checkedMembers" class="at-user-item" :key="m.userId">
|
||||
<head-image :name="m.showNickName" :url="m.headImage" size="mini"></head-image>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="search-bar">
|
||||
<uni-search-bar v-model="searchText" cancelButton="none" radius="100" placeholder="搜索"></uni-search-bar>
|
||||
</view>
|
||||
<view class="member-items">
|
||||
<virtual-scroller :items="memberItems">
|
||||
<template v-slot="{ item }">
|
||||
<view class="member-item" @click="onSwitchChecked(item)">
|
||||
<head-image :name="item.showNickName" :online="item.online" :url="item.headImage"
|
||||
size="small"></head-image>
|
||||
<view class="member-name">{{ item.showNickName }}</view>
|
||||
<radio :checked="item.checked" :disabled="item.locked"
|
||||
@click.stop="onSwitchChecked(item)" />
|
||||
</view>
|
||||
</template>
|
||||
</virtual-scroller>
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "chat-at-box",
|
||||
props: {
|
||||
ownerId: {
|
||||
type: Number,
|
||||
},
|
||||
members: {
|
||||
type: Array
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchText: "",
|
||||
showMembers: []
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
init(atUserIds) {
|
||||
this.showMembers = [];
|
||||
let userId = this.userStore.userInfo.id;
|
||||
if (this.ownerId == userId) {
|
||||
this.showMembers.push({
|
||||
userId: -1,
|
||||
showNickName: "全体成员"
|
||||
})
|
||||
}
|
||||
this.members.forEach((m) => {
|
||||
if (!m.quit && m.userId != userId) {
|
||||
m.checked = atUserIds.indexOf(m.userId) >= 0;
|
||||
this.showMembers.push(m);
|
||||
}
|
||||
});
|
||||
},
|
||||
open() {
|
||||
this.$refs.popup.open();
|
||||
},
|
||||
onSwitchChecked(member) {
|
||||
member.checked = !member.checked;
|
||||
},
|
||||
onClean() {
|
||||
this.showMembers.forEach((m) => {
|
||||
m.checked = false;
|
||||
})
|
||||
},
|
||||
onOk() {
|
||||
this.$refs.popup.close();
|
||||
},
|
||||
onChange(e) {
|
||||
if (!e.show) {
|
||||
this.$emit("complete", this.atUserIds)
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
atUserIds() {
|
||||
return this.showMembers.filter(m => m.checked).map(m => m.userId);
|
||||
},
|
||||
checkedMembers() {
|
||||
return this.showMembers.filter(m => m.checked);
|
||||
},
|
||||
memberItems() {
|
||||
return this.showMembers.filter(m => m.showNickName.includes(this.searchText));
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chat-at-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: white;
|
||||
padding: 10rpx;
|
||||
//border-radius: 15rpx;
|
||||
|
||||
.chat-at-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 70rpx;
|
||||
padding: 10rpx;
|
||||
|
||||
.chat-at-tip {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chat-at-btn {
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.at-user-items {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 90rpx;
|
||||
|
||||
.at-user-item {
|
||||
padding: 3rpx;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.member-items {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.member-item {
|
||||
height: 110rpx;
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding: 0 30rpx;
|
||||
align-items: center;
|
||||
background-color: white;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 1px;
|
||||
|
||||
&:hover {
|
||||
background-color: $im-bg-active;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
flex: 1;
|
||||
padding-left: 20rpx;
|
||||
font-size: $im-font-size;
|
||||
line-height: 60rpx;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<uni-popup ref="popup" type="bottom">
|
||||
<view class="chat-group-readed">
|
||||
<view class="uni-padding-wrap uni-common-mt">
|
||||
<uni-segmented-control :current="current" :values="items" style-type="button"
|
||||
@clickItem="onClickItem" />
|
||||
</view>
|
||||
<view class="content">
|
||||
<view v-if="current === 0">
|
||||
<virtual-scroller :items="readedMembers">
|
||||
<template v-slot="{ item }">
|
||||
<view class="member-item">
|
||||
<head-image :name="item.showNickName" :online="item.online" :url="item.headImage"
|
||||
:size="90"></head-image>
|
||||
<view class="member-name">{{ item.showNickName }}</view>
|
||||
</view>
|
||||
</template>
|
||||
</virtual-scroller>
|
||||
</view>
|
||||
<view v-if="current === 1">
|
||||
<virtual-scroller :items="unreadMembers">
|
||||
<template v-slot="{ item }">
|
||||
<view class="member-item">
|
||||
<head-image :name="item.showNickName" :online="item.online" :url="item.headImage"
|
||||
:size="90"></head-image>
|
||||
<view class="member-name">{{ item.showNickName }}</view>
|
||||
</view>
|
||||
</template>
|
||||
</virtual-scroller>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "chat-group-readed",
|
||||
data() {
|
||||
return {
|
||||
items: ['已读', '未读'],
|
||||
current: 0,
|
||||
readedMembers: [],
|
||||
unreadMembers: []
|
||||
};
|
||||
},
|
||||
props: {
|
||||
msgInfo: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
groupMembers: {
|
||||
type: Array
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.$refs.popup.open();
|
||||
this.loadReadedUser();
|
||||
},
|
||||
loadReadedUser() {
|
||||
this.readedMembers = [];
|
||||
this.unreadMembers = [];
|
||||
this.$http({
|
||||
url: `/message/group/findReadedUsers?groupId=${this.msgInfo.groupId}&messageId=${this.msgInfo.id}`,
|
||||
method: 'Get'
|
||||
}).then(userIds => {
|
||||
this.groupMembers.forEach(member => {
|
||||
// 发送者和已退群的不显示
|
||||
if (member.userId == this.msgInfo.sendId || member.quit) {
|
||||
return;
|
||||
}
|
||||
// 区分已读还是未读
|
||||
if (userIds.find(userId => member.userId == userId)) {
|
||||
this.readedMembers.push(member);
|
||||
} else {
|
||||
this.unreadMembers.push(member);
|
||||
}
|
||||
})
|
||||
this.items[0] = `已读(${this.readedMembers.length})`;
|
||||
this.items[1] = `未读(${this.unreadMembers.length})`;
|
||||
|
||||
let chatInfo = {
|
||||
type: 'GROUP',
|
||||
targetId: this.msgInfo.groupId
|
||||
}
|
||||
let msgInfo = {
|
||||
id: this.msgInfo.id,
|
||||
groupId: this.msgInfo.groupId,
|
||||
readedCount: this.readedMembers.length
|
||||
}
|
||||
// 更新已读人数
|
||||
this.chatStore.updateMessage(msgInfo, chatInfo)
|
||||
})
|
||||
},
|
||||
onClickItem(e) {
|
||||
this.current = e.currentIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chat-group-readed {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: white;
|
||||
padding: 10rpx;
|
||||
|
||||
.member-item {
|
||||
height: 120rpx;
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding: 0 30rpx;
|
||||
align-items: center;
|
||||
background-color: white;
|
||||
white-space: nowrap;
|
||||
|
||||
.member-name {
|
||||
flex: 1;
|
||||
padding-left: 20rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
line-height: 60rpx;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<view class="chat-item" :class="active ? 'active' : ''">
|
||||
<!--rich-text中的表情包会屏蔽事件,所以这里用一个遮罩层捕获点击事件 -->
|
||||
<view class="mask" @tap="showChatBox()"></view>
|
||||
<view class="left">
|
||||
<head-image :url="chat.headImage" :name="chat.showName" :online="online"></head-image>
|
||||
</view>
|
||||
<view class="chat-right">
|
||||
<view class="chat-name">
|
||||
<view class="chat-name-text">
|
||||
<view>{{ chat.showName }}</view>
|
||||
</view>
|
||||
<view class="chat-time">{{ $date.toTimeText(chat.lastSendTime, true) }}</view>
|
||||
</view>
|
||||
<view class="chat-content">
|
||||
<view class="chat-at-text">{{ atText }}</view>
|
||||
<view class="chat-send-name" v-if="isShowSendName">{{
|
||||
chat.sendNickName + ': '
|
||||
}}</view>
|
||||
<view v-if="!isTextMessage" class="chat-content-text">{{ chat.lastContent }}</view>
|
||||
<rich-text v-else class="chat-content-text" :nodes="nodesText"></rich-text>
|
||||
<view v-if="chat.isDnd" class="icon iconfont icon-dnd"></view>
|
||||
<uni-badge v-else-if="chat.unreadCount > 0" :max-num="99" :text="chat.unreadCount" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'chatItem',
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
props: {
|
||||
chat: {
|
||||
type: Object,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showChatBox() {
|
||||
// 初始化期间进入会话会导致消息不刷新
|
||||
if (!this.configStore.appInit || this.chatStore.loading) {
|
||||
uni.showToast({
|
||||
title: '正在初始化页面,请稍后...',
|
||||
icon: 'none',
|
||||
});
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: '/pages/message/chat-box?chatIdx=' + this.index,
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isShowSendName() {
|
||||
if (!this.chat.sendNickName) {
|
||||
return false;
|
||||
}
|
||||
let size = this.chat.messages.length;
|
||||
if (size == 0) {
|
||||
return false;
|
||||
}
|
||||
// 只有群聊的普通消息需要显示名称
|
||||
let lastMsg = this.chat.messages[size - 1];
|
||||
return this.$msgType.isNormal(lastMsg.type);
|
||||
},
|
||||
atText() {
|
||||
if (this.chat.atMe) {
|
||||
return '[有人@我]';
|
||||
} else if (this.chat.atAll) {
|
||||
return '[@全体成员]';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
isTextMessage() {
|
||||
if (this.chat.messages.length == 0) {
|
||||
return false;
|
||||
}
|
||||
let idx = this.chat.messages.length - 1;
|
||||
let messageType = this.chat.messages[idx].type;
|
||||
return messageType == this.$enums.MESSAGE_TYPE.TEXT;
|
||||
},
|
||||
nodesText() {
|
||||
let text = this.$str.html2Escape(this.chat.lastContent);
|
||||
return this.$emo.transform(text, 'emoji-small');
|
||||
},
|
||||
online() {
|
||||
if (this.chat.type == 'PRIVATE') {
|
||||
let friend = this.friendStore.findFriend(this.chat.targetId);
|
||||
return friend && friend.online;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chat-item {
|
||||
height: 96rpx;
|
||||
display: flex;
|
||||
margin-bottom: 2rpx;
|
||||
position: relative;
|
||||
padding: 18rpx 20rpx;
|
||||
align-items: center;
|
||||
background-color: white;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background-color: $im-bg-active;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $im-bg-active;
|
||||
}
|
||||
|
||||
.mask {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.left {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
}
|
||||
|
||||
.chat-right {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-left: 20rpx;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
|
||||
.chat-name {
|
||||
display: flex;
|
||||
|
||||
.chat-name-text {
|
||||
flex: 1;
|
||||
font-size: $im-font-size-large;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-time {
|
||||
font-size: $im-font-size-smaller-extra;
|
||||
color: $im-text-color-lighter;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
display: flex;
|
||||
font-size: $im-font-size-smaller;
|
||||
color: $im-text-color-lighter;
|
||||
padding-top: 8rpx;
|
||||
align-items: center;
|
||||
|
||||
.chat-at-text {
|
||||
color: $im-color-danger;
|
||||
}
|
||||
|
||||
.chat-send-name {
|
||||
font-size: $im-font-size-smaller;
|
||||
}
|
||||
|
||||
.chat-content-text {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: $im-font-size;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,546 @@
|
||||
<template>
|
||||
<view class="chat-message-item">
|
||||
<view class="message-tip" v-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TEXT">
|
||||
{{ msgInfo.content }}
|
||||
</view>
|
||||
<view class="message-tip" v-else-if="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TIME">
|
||||
{{ $date.toTimeText(msgInfo.sendTime) }}
|
||||
</view>
|
||||
<view class="message-normal" v-else-if="isNormal" :class="{ 'message-mine': msgInfo.selfSend }">
|
||||
<head-image class="avatar" @longpress.prevent="$emit('longPressHead')" :id="msgInfo.sendId" :url="headImage"
|
||||
:name="showName" size="small"></head-image>
|
||||
<view class="content">
|
||||
<view v-if="msgInfo.groupId && !msgInfo.selfSend" class="top">
|
||||
<text class="name">{{ showName }}</text>
|
||||
</view>
|
||||
<view class="bottom">
|
||||
<view class="message-content-wrapper">
|
||||
<view v-if="msgInfo.type == $enums.MESSAGE_TYPE.TEXT">
|
||||
<long-press-menu :items="menuItems" @select="onSelectMenu">
|
||||
<!-- up-parse支持点击a标签,但是不支持显示emo表情,也不支持换行 -->
|
||||
<up-parse v-if="$url.containUrl(msgInfo.content)&&!$emo.containEmoji(msgInfo.content)"
|
||||
class="message-text" :showImgMenu="false" :content="nodesText"></up-parse>
|
||||
<!-- rich-text支持显示emo表情以及消息换行,但是不支持点击a标签 -->
|
||||
<rich-text v-else class="message-text" :nodes="nodesText"></rich-text>
|
||||
</long-press-menu>
|
||||
</view>
|
||||
<view class="message-image" v-else-if="msgInfo.type == $enums.MESSAGE_TYPE.IMAGE">
|
||||
<long-press-menu :items="menuItems" @select="onSelectMenu">
|
||||
<view class="image-box">
|
||||
<image class="send-image" :style="imageStyle" mode="aspectFill"
|
||||
:src="contentData.thumbUrl" lazy-load="true" @click.stop="onShowFullImage()">
|
||||
</image>
|
||||
<loading v-if="sending"></loading>
|
||||
</view>
|
||||
</long-press-menu>
|
||||
</view>
|
||||
<view class="message-file" v-else-if="msgInfo.type == $enums.MESSAGE_TYPE.FILE">
|
||||
<long-press-menu :items="menuItems" @select="onSelectMenu">
|
||||
<view class="file-box">
|
||||
<view class="file-info">
|
||||
<uni-link class="file-name" :text="contentData.name" showUnderLine="true"
|
||||
color="#007BFF" :href="contentData.url"></uni-link>
|
||||
<view class="file-size">{{ fileSize }}</view>
|
||||
</view>
|
||||
<view class="file-icon iconfont icon-file"></view>
|
||||
<loading v-if="sending"></loading>
|
||||
</view>
|
||||
</long-press-menu>
|
||||
</view>
|
||||
<long-press-menu v-else-if="msgInfo.type == $enums.MESSAGE_TYPE.AUDIO" :items="menuItems"
|
||||
@select="onSelectMenu">
|
||||
<view class="message-audio message-text" @click="onPlayAudio()">
|
||||
<text class="iconfont icon-voice-play"></text>
|
||||
<text class="chat-audio-text">{{ contentData.duration + '"' }}</text>
|
||||
<text v-if="audioPlayState == 'PAUSE'" class="iconfont icon-play"></text>
|
||||
<text v-if="audioPlayState == 'PLAYING'" class="iconfont icon-pause"></text>
|
||||
</view>
|
||||
</long-press-menu>
|
||||
<long-press-menu v-if="isAction" :items="menuItems" @select="onSelectMenu">
|
||||
<view class="chat-realtime message-text" @click="$emit('call')">
|
||||
<text v-if="msgInfo.type == $enums.MESSAGE_TYPE.ACT_RT_VOICE"
|
||||
class="iconfont icon-chat-voice"></text>
|
||||
<text v-if="msgInfo.type == $enums.MESSAGE_TYPE.ACT_RT_VIDEO"
|
||||
class="iconfont icon-chat-video"></text>
|
||||
<text>{{ msgInfo.content }}</text>
|
||||
</view>
|
||||
</long-press-menu>
|
||||
<view v-if="sending&&(isTextMessage||isAudioMessage)" class="sending">
|
||||
<loading :size="40" icon-color="#656adf" :mask="false"></loading>
|
||||
</view>
|
||||
<view v-else-if="sendFail" @click="onSendFail"
|
||||
class="send-fail iconfont icon-warning-circle-fill"></view>
|
||||
</view>
|
||||
<view class="message-status" v-if="!isAction && msgInfo.selfSend && !msgInfo.groupId">
|
||||
<text class="chat-readed" v-if="msgInfo.status == $enums.MESSAGE_STATUS.READED">已读</text>
|
||||
<text class="chat-unread" v-else>未读</text>
|
||||
</view>
|
||||
<view class="chat-receipt" v-if="msgInfo.receipt" @click="onShowReadedBox">
|
||||
<text v-if="msgInfo.receiptOk" class="tool-icon iconfont icon-ok"></text>
|
||||
<text v-else>{{ msgInfo.readedCount }}人已读</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<chat-group-readed ref="chatGroupReaded" :groupMembers="groupMembers" :msgInfo="msgInfo"></chat-group-readed>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "chat-message-item",
|
||||
props: {
|
||||
headImage: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
showName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
msgInfo: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
groupMembers: {
|
||||
type: Array
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
audioPlayState: 'STOP',
|
||||
innerAudioContext: null,
|
||||
menu: {
|
||||
show: false,
|
||||
style: ""
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSendFail() {
|
||||
this.$emit("resend", this.msgInfo);
|
||||
},
|
||||
onPlayAudio() {
|
||||
// 初始化音频播放器
|
||||
if (!this.innerAudioContext) {
|
||||
this.innerAudioContext = uni.createInnerAudioContext();
|
||||
let url = this.contentData.url;
|
||||
this.innerAudioContext.src = url;
|
||||
this.innerAudioContext.onEnded((e) => {
|
||||
console.log('停止')
|
||||
this.audioPlayState = "STOP"
|
||||
this.emit();
|
||||
})
|
||||
this.innerAudioContext.onError((e) => {
|
||||
this.audioPlayState = "STOP"
|
||||
console.log("播放音频出错");
|
||||
console.log(e)
|
||||
this.emit();
|
||||
});
|
||||
}
|
||||
if (this.audioPlayState == 'STOP') {
|
||||
this.innerAudioContext.play();
|
||||
this.audioPlayState = "PLAYING";
|
||||
} else if (this.audioPlayState == 'PLAYING') {
|
||||
this.innerAudioContext.pause();
|
||||
this.audioPlayState = "PAUSE"
|
||||
} else if (this.audioPlayState == 'PAUSE') {
|
||||
this.innerAudioContext.play();
|
||||
this.audioPlayState = "PLAYING"
|
||||
}
|
||||
this.emit();
|
||||
},
|
||||
onSelectMenu(item) {
|
||||
this.$emit(item.key.toLowerCase(), this.msgInfo);
|
||||
this.menu.show = false;
|
||||
},
|
||||
onShowFullImage() {
|
||||
let imageUrl = this.contentData.originUrl;
|
||||
uni.previewImage({
|
||||
urls: [imageUrl]
|
||||
})
|
||||
},
|
||||
onShowReadedBox() {
|
||||
this.$refs.chatGroupReaded.open();
|
||||
},
|
||||
emit() {
|
||||
this.$emit("audioStateChange", this.audioPlayState, this.msgInfo);
|
||||
},
|
||||
stopPlayAudio() {
|
||||
if (this.innerAudioContext) {
|
||||
this.innerAudioContext.stop();
|
||||
this.innerAudioContext = null;
|
||||
this.audioPlayState = "STOP"
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sending() {
|
||||
return this.msgInfo.status == this.$enums.MESSAGE_STATUS.SENDING;
|
||||
},
|
||||
sendFail() {
|
||||
return this.msgInfo.status == this.$enums.MESSAGE_STATUS.FAILED;
|
||||
},
|
||||
contentData() {
|
||||
return JSON.parse(this.msgInfo.content)
|
||||
},
|
||||
fileSize() {
|
||||
let size = this.contentData.size;
|
||||
if (size > 1024 * 1024) {
|
||||
return Math.round(size / 1024 / 1024) + "M";
|
||||
}
|
||||
if (size > 1024) {
|
||||
return Math.round(size / 1024) + "KB";
|
||||
}
|
||||
return size + "B";
|
||||
},
|
||||
menuItems() {
|
||||
let items = [];
|
||||
if (this.msgInfo.type == this.$enums.MESSAGE_TYPE.TEXT) {
|
||||
items.push({
|
||||
key: 'COPY',
|
||||
name: '复制',
|
||||
icon: 'bars'
|
||||
});
|
||||
}
|
||||
if (this.msgInfo.selfSend && this.msgInfo.id > 0) {
|
||||
items.push({
|
||||
key: 'RECALL',
|
||||
name: '撤回',
|
||||
icon: 'refreshempty'
|
||||
});
|
||||
}
|
||||
items.push({
|
||||
key: 'DELETE',
|
||||
name: '删除',
|
||||
icon: 'trash',
|
||||
color: '#e64e4e'
|
||||
});
|
||||
if (this.msgInfo.type == this.$enums.MESSAGE_TYPE.FILE) {
|
||||
items.push({
|
||||
key: 'DOWNLOAD',
|
||||
name: '下载并打开',
|
||||
icon: 'download'
|
||||
});
|
||||
}
|
||||
return items;
|
||||
},
|
||||
isTextMessage() {
|
||||
return this.msgInfo.type == this.$enums.MESSAGE_TYPE.TEXT
|
||||
},
|
||||
isAudioMessage() {
|
||||
return this.msgInfo.type == this.$enums.MESSAGE_TYPE.AUDIO
|
||||
},
|
||||
isAction() {
|
||||
return this.$msgType.isAction(this.msgInfo.type);
|
||||
},
|
||||
isNormal() {
|
||||
const type = this.msgInfo.type;
|
||||
return this.$msgType.isNormal(type) || this.$msgType.isAction(type)
|
||||
},
|
||||
nodesText() {
|
||||
let color = this.msgInfo.selfSend ? 'white' : '';
|
||||
let text = this.$str.html2Escape(this.msgInfo.content)
|
||||
text = this.$url.replaceURLWithHTMLLinks(text, color)
|
||||
return this.$emo.transform(text, 'emoji-normal')
|
||||
},
|
||||
imageStyle() {
|
||||
console.log(uni.getSystemInfo())
|
||||
let maxSize = uni.getSystemInfoSync().windowWidth * 0.6;
|
||||
let minSize = uni.getSystemInfoSync().windowWidth * 0.2;
|
||||
let width = this.contentData.width;
|
||||
let height = this.contentData.height;
|
||||
if (width && height) {
|
||||
let ratio = Math.min(width, height) / Math.max(width, height);
|
||||
let w = Math.max(width > height ? maxSize : ratio * maxSize, minSize);
|
||||
let h = Math.max(width > height ? ratio * maxSize : maxSize, minSize);
|
||||
return `width: ${w}px;height:${h}px;`
|
||||
} else {
|
||||
// 兼容历史版本,历史数据没有记录宽高
|
||||
return `max-width: ${maxSize}px;min-width:100px;max-height: ${maxSize}px;min-height:100px;`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chat-message-item {
|
||||
padding: 2rpx 20rpx;
|
||||
|
||||
.message-tip {
|
||||
line-height: 60rpx;
|
||||
text-align: center;
|
||||
color: $im-text-color-lighter;
|
||||
font-size: $im-font-size-smaller-extra;
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.message-normal {
|
||||
position: relative;
|
||||
margin-bottom: 22rpx;
|
||||
padding-left: 110rpx;
|
||||
min-height: 80rpx;
|
||||
|
||||
.avatar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
text-align: left;
|
||||
|
||||
.top {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
|
||||
.name {
|
||||
color: $im-text-color-lighter;
|
||||
font-size: $im-font-size-smaller;
|
||||
line-height: $im-font-size-smaller;
|
||||
height: $im-font-size-smaller;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
display: inline-block;
|
||||
padding-right: 80rpx;
|
||||
margin-top: 5rpx;
|
||||
|
||||
.message-content-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.sending {
|
||||
position: relative;
|
||||
margin: 0 6rpx;
|
||||
|
||||
.icon-loading {
|
||||
color: $im-color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.send-fail {
|
||||
color: #e60c0c;
|
||||
font-size: 50rpx;
|
||||
margin: 0 5rpx;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
position: relative;
|
||||
line-height: 1.6;
|
||||
margin-top: 10rpx;
|
||||
padding: 16rpx 24rpx;
|
||||
background-color: $im-bg;
|
||||
border-radius: 20rpx;
|
||||
color: $im-text-color;
|
||||
font-size: $im-font-size;
|
||||
text-align: left;
|
||||
display: block;
|
||||
word-break: break-word;
|
||||
white-space: pre-line;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -20rpx;
|
||||
top: 26rpx;
|
||||
width: 6rpx;
|
||||
height: 6rpx;
|
||||
border-style: solid dashed dashed;
|
||||
border-color: $im-bg transparent transparent;
|
||||
overflow: hidden;
|
||||
border-width: 18rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.message-image {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.image-box {
|
||||
position: relative;
|
||||
|
||||
.send-image {
|
||||
cursor: pointer;
|
||||
border-radius: 10rpx;
|
||||
background: $im-bg;
|
||||
border: 6rpx solid $im-color-primary-light-5;
|
||||
}
|
||||
}
|
||||
|
||||
.send-fail {
|
||||
color: $im-color-danger;
|
||||
font-size: $im-font-size;
|
||||
cursor: pointer;
|
||||
margin: 0 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.message-file {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
.file-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
min-height: 60px;
|
||||
border-radius: 4px;
|
||||
padding: 10px 15px;
|
||||
box-shadow: $im-box-shadow-dark;
|
||||
|
||||
.file-info {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
width: 300rpx;
|
||||
|
||||
.file-name {
|
||||
font-weight: 600;
|
||||
margin-bottom: 15px;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 80rpx;
|
||||
color: #d42e07;
|
||||
}
|
||||
}
|
||||
|
||||
.send-fail {
|
||||
color: #e60c0c;
|
||||
font-size: 50rpx;
|
||||
cursor: pointer;
|
||||
margin: 0 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.message-audio {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.chat-audio-text {
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.icon-voice-play {
|
||||
font-size: 18px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-realtime {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.iconfont {
|
||||
font-size: 20px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.message-status {
|
||||
line-height: $im-font-size-smaller-extra;
|
||||
font-size: $im-font-size-smaller-extra;
|
||||
padding-top: 2rpx;
|
||||
|
||||
.chat-readed {
|
||||
display: block;
|
||||
padding-top: 2rpx;
|
||||
color: $im-text-color-lighter;
|
||||
}
|
||||
|
||||
.chat-unread {
|
||||
color: $im-color-danger;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-receipt {
|
||||
font-size: $im-font-size-smaller;
|
||||
color: $im-text-color-lighter;
|
||||
font-weight: 600;
|
||||
|
||||
.icon-ok {
|
||||
font-size: 20px;
|
||||
color: $im-color-success;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.message-mine {
|
||||
text-align: right;
|
||||
padding-left: 0;
|
||||
padding-right: 110rpx;
|
||||
|
||||
.avatar {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
text-align: right;
|
||||
|
||||
.bottom {
|
||||
padding-left: 80rpx;
|
||||
padding-right: 0;
|
||||
|
||||
.message-content-wrapper {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
margin-left: 10px;
|
||||
background-color: $im-color-primary-light-2;
|
||||
color: #fff;
|
||||
|
||||
&:after {
|
||||
left: auto;
|
||||
right: -9px;
|
||||
border-top-color: $im-color-primary-light-2;
|
||||
}
|
||||
}
|
||||
|
||||
.message-audio {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.chat-audio-text {
|
||||
padding-right: 0;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.icon-voice-play {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.chat-realtime {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.iconfont {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<view class="chat-record">
|
||||
<view class="chat-record-bar" :class="{ recording: recording }" id="chat-record-bar" @click.stop=""
|
||||
@touchstart.prevent="onStartRecord" @touchmove.prevent="onTouchMove" @touchend.prevent="onEndRecord">
|
||||
{{ recording ? '正在录音' : '长按 说话' }}</view>
|
||||
<view v-if="recording" class="chat-record-window" :style="recordWindowStyle">
|
||||
<view class="rc-wave">
|
||||
<text class="note" style="--d: 0"></text>
|
||||
<text class="note" style="--d: 1"></text>
|
||||
<text class="note" style="--d: 2"></text>
|
||||
<text class="note" style="--d: 3"></text>
|
||||
<text class="note" style="--d: 4"></text>
|
||||
<text class="note" style="--d: 5"></text>
|
||||
<text class="note" style="--d: 6"></text>
|
||||
</view>
|
||||
<view class="rc-tip">{{ recordTip }}</view>
|
||||
<view class="cancel-btn" @click="onCancel">
|
||||
<uni-icons :class="moveToCancel ? 'red' : 'black'" type="clear" :size="moveToCancel ? 45 : 40"></uni-icons>
|
||||
</view>
|
||||
<view class="opt-tip" :class="moveToCancel ? 'red' : 'black'">{{ moveToCancel ? '松手取消' : '松手发送,上划取消' }}</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "chat-record",
|
||||
data() {
|
||||
return {
|
||||
recording: false,
|
||||
moveToCancel: false,
|
||||
recordBarTop: 0,
|
||||
druation: 0,
|
||||
rcTimer: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onTouchMove(e) {
|
||||
const moveY = e.touches[0].clientY;
|
||||
this.moveToCancel = moveY < this.recordBarTop - 40;
|
||||
},
|
||||
onCancel() {
|
||||
if (this.recording) {
|
||||
this.moveToCancel = true;
|
||||
this.onEndRecord();
|
||||
}
|
||||
},
|
||||
onStartRecord() {
|
||||
/* 用户第一次使用语音会唤醒录音权限请求,此时会导致@touchend失效,
|
||||
一直处于录音状态,这里允许用户再次点击发送语音并结束录音 */
|
||||
if (this.recording) {
|
||||
return;
|
||||
}
|
||||
console.log("开始录音")
|
||||
this.moveToCancel = false;
|
||||
this.initRecordBar();
|
||||
if(!this.$rc.checkIsEnable()){
|
||||
return;
|
||||
}
|
||||
this.$rc.start().then(() => {
|
||||
this.recording = true;
|
||||
console.log("开始录音成功")
|
||||
// 开始计时
|
||||
this.startTimer();
|
||||
}).catch((e) => {
|
||||
console.log("录音失败" + JSON.stringify(e))
|
||||
uni.showToast({
|
||||
title: "录音失败",
|
||||
icon: "none"
|
||||
});
|
||||
});
|
||||
},
|
||||
onEndRecord() {
|
||||
if(!this.recording){
|
||||
return;
|
||||
}
|
||||
this.recording = false;
|
||||
// 停止计时
|
||||
this.stopTimer();
|
||||
// 停止录音
|
||||
this.$rc.close();
|
||||
// 触屏位置是否移动到了取消区域
|
||||
if (this.moveToCancel) {
|
||||
console.log("录音取消")
|
||||
return;
|
||||
}
|
||||
// 大于1秒才发送
|
||||
if (this.druation <= 1) {
|
||||
uni.showToast({
|
||||
title: "说话时间太短",
|
||||
icon: 'none'
|
||||
})
|
||||
return;
|
||||
}
|
||||
this.$rc.upload().then((data) => {
|
||||
this.$emit("send", data);
|
||||
}).catch((e) => {
|
||||
uni.showToast({
|
||||
title: e,
|
||||
icon: 'none'
|
||||
})
|
||||
})
|
||||
},
|
||||
startTimer() {
|
||||
this.druation = 0;
|
||||
this.stopTimer();
|
||||
this.rcTimer = setInterval(() => {
|
||||
this.druation++;
|
||||
// 大于60s,直接结束
|
||||
if (this.druation >= 60) {
|
||||
this.onEndRecord();
|
||||
}
|
||||
}, 1000)
|
||||
},
|
||||
stopTimer() {
|
||||
this.rcTimer && clearInterval(this.rcTimer);
|
||||
this.rcTimer = null;
|
||||
},
|
||||
initRecordBar() {
|
||||
const query = uni.createSelectorQuery().in(this);
|
||||
query.select('#chat-record-bar').boundingClientRect((rect) => {
|
||||
// 顶部高度位置
|
||||
this.recordBarTop = rect.top;
|
||||
}).exec()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
recordWindowStyle() {
|
||||
const windowHeight = uni.getSystemInfoSync().windowHeight;
|
||||
const bottom = windowHeight - this.recordBarTop + 12;
|
||||
return `bottom:${bottom}px;`
|
||||
},
|
||||
recordTip() {
|
||||
if (this.druation > 50) {
|
||||
return `${60 - this.druation}s后将停止录音`;
|
||||
}
|
||||
return `录音时长:${this.druation}s`;
|
||||
}
|
||||
},
|
||||
unmounted() {
|
||||
this.stopTimer();
|
||||
this.recording = false;
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chat-record {
|
||||
.rc-wave {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
height: 80rpx;
|
||||
|
||||
.note {
|
||||
background: linear-gradient(to top, $im-color-primary-light-1 0%, $im-color-primary-light-6 100%);
|
||||
width: 4px;
|
||||
height: 50%;
|
||||
border-radius: 5rpx;
|
||||
margin-right: 4px;
|
||||
animation: loading 0.5s infinite linear;
|
||||
animation-delay: calc(0.1s * var(--d));
|
||||
|
||||
@keyframes loading {
|
||||
0% {
|
||||
background-image: linear-gradient(to right, $im-color-primary-light-1 0%, $im-color-primary-light-6 100%);
|
||||
height: 20%;
|
||||
border-radius: 5rpx;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-image: linear-gradient(to top, $im-color-primary-light-1 0%, $im-color-primary-light-6 100%);
|
||||
height: 80%;
|
||||
border-radius: 5rpx;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-image: linear-gradient(to top, $im-color-primary-light-1 0%, $im-color-primary-light-6 100%);
|
||||
height: 20%;
|
||||
border-radius: 5rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-record-bar {
|
||||
padding: 10rpx;
|
||||
margin: 10rpx;
|
||||
border-radius: 10rpx;
|
||||
text-align: center;
|
||||
box-shadow: $im-box-shadow;
|
||||
|
||||
&.recording {
|
||||
background-color: $im-color-primary;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-record-window {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 360rpx;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
padding: 30rpx;
|
||||
|
||||
.icon-microphone {
|
||||
text-align: center;
|
||||
font-size: 80rpx;
|
||||
padding: 10rpx;
|
||||
|
||||
}
|
||||
|
||||
.rc-tip {
|
||||
text-align: center;
|
||||
font-size: $im-font-size-small;
|
||||
color: $im-text-color-light;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
text-align: center;
|
||||
margin-top: 40rpx;
|
||||
height: 80rpx;
|
||||
|
||||
}
|
||||
|
||||
.opt-tip {
|
||||
text-align: center;
|
||||
font-size: 30rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: $im-color-danger !important;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<view>
|
||||
<lsj-upload ref="lsjUpload" :height="'100%'" :option="option" @uploadEnd="onUploadEnd" @change="onChange"
|
||||
:size="maxSize" :instantly="true">
|
||||
<slot></slot>
|
||||
</lsj-upload>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UNI_APP from '@/.env.js';
|
||||
|
||||
export default {
|
||||
name: "file-upload",
|
||||
data() {
|
||||
return {
|
||||
fileMap: new Map(),
|
||||
option: {
|
||||
url: UNI_APP.BASE_URL + '/file/upload',
|
||||
name: 'file',
|
||||
header: {
|
||||
accessToken: uni.getStorageSync('loginInfo').accessToken
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
onBefore: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
onSuccess: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
onError: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.$refs.lsjUpload.show();
|
||||
},
|
||||
hide() {
|
||||
this.$refs.lsjUpload.hide();
|
||||
},
|
||||
onUploadEnd(item) {
|
||||
let file = this.fileMap.get(item.path);
|
||||
if (item.type == 'fail') {
|
||||
this.onError(file)
|
||||
return;
|
||||
}
|
||||
let res = JSON.parse(item.responseText);
|
||||
if (res.code == 200) {
|
||||
// 上传成功
|
||||
this.onOk(file, res);
|
||||
} else if (res.code == 401) {
|
||||
// token已过期,重新获取token
|
||||
this.refreshToken().then((res) => {
|
||||
let newToken = res.data.accessToken;
|
||||
this.option.header.accessToken = newToken;
|
||||
this.$refs.lsjUpload.setData(this.option);
|
||||
// 重新上传
|
||||
this.$refs.lsjUpload.upload(file.name);
|
||||
}).catch(() => {
|
||||
this.onError(file, res);
|
||||
})
|
||||
} else {
|
||||
// 上传失败
|
||||
this.onError(file, res);
|
||||
}
|
||||
},
|
||||
onChange(files) {
|
||||
if (!files.size) {
|
||||
return;
|
||||
}
|
||||
files.forEach((file, name) => {
|
||||
if (!this.fileMap.has(file.path)) {
|
||||
this.onBefore && this.onBefore(file)
|
||||
this.fileMap.set(file.path, file);
|
||||
}
|
||||
})
|
||||
},
|
||||
onOk(file, res) {
|
||||
this.fileMap.delete(file.path);
|
||||
this.$refs.lsjUpload.clear(file.name);
|
||||
this.onSuccess && this.onSuccess(file, res);
|
||||
},
|
||||
onFailed(file, res) {
|
||||
this.fileMap.delete(file.path);
|
||||
this.$refs.lsjUpload.clear(file.name);
|
||||
this.onError && this.onError(file, res);
|
||||
},
|
||||
refreshToken() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let loginInfo = uni.getStorageSync('loginInfo')
|
||||
uni.request({
|
||||
method: 'PUT',
|
||||
url: UNI_APP.BASE_URL + '/refreshToken',
|
||||
header: {
|
||||
refreshToken: loginInfo.refreshToken
|
||||
},
|
||||
success: (res) => {
|
||||
resolve(res.data);
|
||||
},
|
||||
fail: (res) => {
|
||||
reject(res);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<view class="friend-item" @click="showFriendInfo()">
|
||||
<head-image :name="friend.nickName" :online="friend.online" :url="friend.headImage" size="small"></head-image>
|
||||
<view class="friend-info">
|
||||
<view class="friend-name">{{ friend.nickName }}</view>
|
||||
<view class="friend-online">
|
||||
<image v-show="friend.onlineWeb" class="online" src="/static/image/online_web.png" title="电脑设备在线" />
|
||||
<image v-show="friend.onlineApp" class="online" src="/static/image/online_app.png" title="移动设备在线" />
|
||||
</view>
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "frined-item",
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
showFriendInfo() {
|
||||
if (this.detail) {
|
||||
uni.navigateTo({
|
||||
url: "/pages/common/user-info?id=" + this.friend.id
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
friend: {
|
||||
type: Object
|
||||
},
|
||||
detail: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scope lang="scss">
|
||||
.friend-item {
|
||||
height: 90rpx;
|
||||
display: flex;
|
||||
margin-bottom: 1rpx;
|
||||
position: relative;
|
||||
padding: 10rpx;
|
||||
padding-left: 20rpx;
|
||||
align-items: center;
|
||||
background-color: white;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background-color: $im-bg;
|
||||
}
|
||||
|
||||
.friend-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
padding-left: 20rpx;
|
||||
text-align: left;
|
||||
|
||||
.friend-name {
|
||||
flex: 1;
|
||||
font-size: $im-font-size;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.friend-online {
|
||||
margin-top: 4rpx;
|
||||
|
||||
.online {
|
||||
padding-right: 4rpx;
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<view class="group-item" @click="showGroupInfo()">
|
||||
<head-image :name="group.showGroupName" :url="group.headImageThumb" size="small"></head-image>
|
||||
<view class="group-name">
|
||||
<view>{{ group.showGroupName }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "group-item",
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
showGroupInfo() {
|
||||
uni.navigateTo({
|
||||
url: "/pages/group/group-info?id=" + this.group.id
|
||||
})
|
||||
},
|
||||
},
|
||||
props: {
|
||||
group: {
|
||||
type: Object
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scope lang="scss">
|
||||
.group-item {
|
||||
height: 90rpx;
|
||||
display: flex;
|
||||
margin-bottom: 2rpx;
|
||||
position: relative;
|
||||
padding: 18rpx 20rpx;
|
||||
align-items: center;
|
||||
background-color: white;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background-color: $im-bg-active;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $im-bg-active;
|
||||
}
|
||||
|
||||
.group-name {
|
||||
font-size: $im-font-size;
|
||||
padding-left: 20rpx;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<uni-popup ref="popup" type="bottom">
|
||||
<view class="group-member-selector">
|
||||
<view class="top-bar">
|
||||
<view class="top-tip">选择成员</view>
|
||||
<button class="top-btn" type="warn" size="mini" @click="onClean()">清空 </button>
|
||||
<button class="top-btn" type="primary" size="mini" @click="onOk()">确定({{ checkedIds.length }})
|
||||
</button>
|
||||
</view>
|
||||
<scroll-view v-show="checkedIds.length > 0" scroll-x="true" scroll-left="120">
|
||||
<view class="checked-users">
|
||||
<view v-for="m in checkedMembers" class="user-item" :key="m.userId">
|
||||
<head-image :name="m.showNickName" :url="m.headImage" :size="60"></head-image>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="search-bar">
|
||||
<uni-search-bar v-model="searchText" cancelButton="none" placeholder="搜索"></uni-search-bar>
|
||||
</view>
|
||||
<view class="member-items">
|
||||
<virtual-scroller :items="showMembers">
|
||||
<template v-slot="{ item }">
|
||||
<view class="member-item" @click="onSwitchChecked(item)">
|
||||
<head-image :name="item.showNickName" :online="item.online" :url="item.headImage"
|
||||
:size="90"></head-image>
|
||||
<view class="member-name">{{ item.showNickName }}
|
||||
</view>
|
||||
<view class="member-checked">
|
||||
<radio :checked="item.checked" :disabled="item.locked"
|
||||
@click.stop="onSwitchChecked(item)" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</virtual-scroller>
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "group-member-selector",
|
||||
props: {
|
||||
group: {
|
||||
type: Object
|
||||
},
|
||||
members: {
|
||||
type: Array
|
||||
},
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: 50
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchText: "",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
init(checkedIds, lockedIds, hideIds) {
|
||||
this.members.forEach((m) => {
|
||||
m.checked = checkedIds.indexOf(m.userId) >= 0;
|
||||
m.locked = lockedIds.indexOf(m.userId) >= 0;
|
||||
m.hide = hideIds.indexOf(m.userId) >= 0;
|
||||
});
|
||||
},
|
||||
open() {
|
||||
this.$refs.popup.open();
|
||||
},
|
||||
onSwitchChecked(m) {
|
||||
if (!m.locked) {
|
||||
m.checked = !m.checked;
|
||||
}
|
||||
// 达到选择上限
|
||||
if (this.maxSize > 0 && this.checkedIds.length > this.maxSize) {
|
||||
m.checked = false;
|
||||
uni.showToast({
|
||||
title: `最多选择${this.maxSize}位用户`,
|
||||
icon: "none"
|
||||
})
|
||||
}
|
||||
},
|
||||
onClean() {
|
||||
this.members.forEach((m) => {
|
||||
if (!m.locked && m.checked) {
|
||||
m.checked = false;
|
||||
}
|
||||
})
|
||||
},
|
||||
onOk() {
|
||||
this.$refs.popup.close();
|
||||
this.$emit("complete", this.checkedIds)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
checkedIds() {
|
||||
return this.checkedMembers.map(m => m.userId)
|
||||
},
|
||||
checkedMembers() {
|
||||
return this.members.filter((m) => !m.quit && !m.hide && m.checked);
|
||||
},
|
||||
showMembers() {
|
||||
return this.members.filter(m => !m.quit && !m.hide && m.showNickName.includes(this.searchText))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.group-member-selector {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: white;
|
||||
padding: 10rpx;
|
||||
border-radius: 15rpx 15rpx 0 0;
|
||||
overflow: hidden;
|
||||
|
||||
.top-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 70rpx;
|
||||
padding: 10rpx 30rpx;
|
||||
|
||||
.top-tip {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.top-btn {
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.checked-users {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 90rpx;
|
||||
padding: 0 30rpx;
|
||||
|
||||
.user-item {
|
||||
padding: 3rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.member-items {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.member-item {
|
||||
height: 120rpx;
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding: 0 30rpx;
|
||||
align-items: center;
|
||||
background-color: white;
|
||||
white-space: nowrap;
|
||||
|
||||
.member-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
padding-left: 20rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
line-height: 60rpx;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
.uni-tag {
|
||||
margin-left: 5rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<uni-popup ref="popup" type="center">
|
||||
<uni-popup-dialog mode="base" :duration="2000" title="是否加入通话?" confirmText="加入" @confirm="onOk">
|
||||
<div class="group-rtc-join">
|
||||
<div class="host-info">
|
||||
<div>发起人</div>
|
||||
<head-image :name="rtcInfo.host.nickName" :url="rtcInfo.host.headImage" :size="80"></head-image>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<div>{{ rtcInfo.userInfos.length + '人正在通话中' }}</div>
|
||||
<scroll-view scroll-x="true" scroll-left="120">
|
||||
<view class="user-list">
|
||||
<view v-for="user in rtcInfo.userInfos" class="user-item" :key="user.id">
|
||||
<head-image :name="user.nickName" :url="user.headImage" :size="80"></head-image>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</div>
|
||||
</div>
|
||||
</uni-popup-dialog>
|
||||
</uni-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
rtcInfo: {}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
groupId: {
|
||||
type: Number
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(rtcInfo) {
|
||||
this.rtcInfo = rtcInfo;
|
||||
this.$refs.popup.open();
|
||||
},
|
||||
onOk() {
|
||||
let users = this.rtcInfo.userInfos;
|
||||
let mine = this.userStore.userInfo;
|
||||
// 加入自己的信息
|
||||
if (!users.find((user) => user.id == mine.id)) {
|
||||
users.push({
|
||||
id: mine.id,
|
||||
nickName: mine.nickName,
|
||||
headImage: mine.headImageThumb,
|
||||
isCamera: false,
|
||||
isMicroPhone: true
|
||||
})
|
||||
}
|
||||
const userInfos = encodeURIComponent(JSON.stringify(users));
|
||||
uni.navigateTo({
|
||||
url: `/pages/chat/chat-group-video?groupId=${this.groupId}&isHost=false
|
||||
&inviterId=${mine.id}&userInfos=${userInfos}`
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.group-rtc-join {
|
||||
width: 100%;
|
||||
|
||||
.host-info {
|
||||
font-size: 16px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
font-size: 16px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.user-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 90rpx;
|
||||
|
||||
.user-item {
|
||||
padding: 3rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<view class="head-image none-pointer-events" @click="showUserInfo($event)" :title="name">
|
||||
<image class="avatar-image" v-if="url" :src="url" :style="avatarImageStyle" lazy-load="true"
|
||||
mode="aspectFill" />
|
||||
<view class="avatar-text" v-if="!url" :style="avatarTextStyle">
|
||||
{{ name?.substring(0, 1).toUpperCase() }}
|
||||
</view>
|
||||
<view v-if="online" class="online" title="用户当前在线">
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "head-image",
|
||||
data() {
|
||||
return {
|
||||
colors: ["#5daa31", "#c7515a", "#e03697", "#85029b",
|
||||
"#c9b455", "#326eb6"
|
||||
]
|
||||
}
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: Number
|
||||
},
|
||||
size: {
|
||||
type: [Number, String],
|
||||
default: 'default'
|
||||
},
|
||||
url: {
|
||||
type: String
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
radius: {
|
||||
type: String,
|
||||
default: "50%"
|
||||
},
|
||||
online: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showUserInfo(e) {
|
||||
if (this.id && this.id > 0) {
|
||||
uni.navigateTo({
|
||||
url: "/pages/common/user-info?id=" + this.id
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
_size() {
|
||||
if (typeof this.size === 'number') {
|
||||
return this.size;
|
||||
} else if (typeof this.size === 'string') {
|
||||
return {
|
||||
'default': 96,
|
||||
'small': 84,
|
||||
'smaller': 72,
|
||||
'mini': 60,
|
||||
'minier': 48,
|
||||
'lage': 108,
|
||||
'lager': 120,
|
||||
} [this.size]
|
||||
}
|
||||
},
|
||||
avatarImageStyle() {
|
||||
return `width:${this._size}rpx;
|
||||
height:${this._size}rpx;`
|
||||
},
|
||||
avatarTextStyle() {
|
||||
return `width: ${this._size}rpx;
|
||||
height:${this._size}rpx;
|
||||
background: linear-gradient(145deg,#ffffff20 25%,#00000060),${this.textColor};
|
||||
font-size:${this._size * 0.45}rpx;
|
||||
border-radius: ${this.radius};
|
||||
`
|
||||
},
|
||||
textColor() {
|
||||
if (!this.name) {
|
||||
return '#fff';
|
||||
}
|
||||
let hash = 0;
|
||||
for (var i = 0; i < this.name.length; i++) {
|
||||
hash += this.name.charCodeAt(i);
|
||||
}
|
||||
return this.colors[hash % this.colors.length];
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.head-image {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
.avatar-image {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.online {
|
||||
position: absolute;
|
||||
right: -10%;
|
||||
bottom: 0;
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
background: limegreen;
|
||||
border-radius: 50%;
|
||||
border: 6rpx solid white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<view @click="selectAndUpload()">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UNI_APP from '@/.env.js'
|
||||
|
||||
export default {
|
||||
name: "image-upload",
|
||||
data() {
|
||||
return {
|
||||
uploadHeaders: {
|
||||
"accessToken": uni.getStorageSync('loginInfo').accessToken
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
maxCount: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: 5 * 1024 * 1024
|
||||
},
|
||||
sourceType: {
|
||||
type: String,
|
||||
default: 'album'
|
||||
},
|
||||
isPermanent: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
thumbSize: {
|
||||
type: Number,
|
||||
default: 50
|
||||
},
|
||||
onBefore: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
onSuccess: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
onError: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectAndUpload() {
|
||||
uni.chooseImage({
|
||||
count: this.maxCount, //最多可以选择的图片张数,默认9
|
||||
sourceType: [this.sourceType], //album 从相册选图,camera 使用相机,默认二者都有。如需直接开相机或直接选相册,请只使用一个选项
|
||||
sizeType: ['original'], //original 原图,compressed 压缩图,默认二者都有
|
||||
success: (res) => {
|
||||
res.tempFiles.forEach((file) => {
|
||||
if (!this.onBefore || this.onBefore(file)) {
|
||||
// 调用上传图片的接口
|
||||
this.uploadImage(file);
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
uploadImage(file) {
|
||||
let action = `/image/upload?isPermanent=${this.isPermanent}&thumbSize=${this.thumbSize}`
|
||||
uni.uploadFile({
|
||||
url: UNI_APP.BASE_URL + action,
|
||||
header: {
|
||||
accessToken: uni.getStorageSync("loginInfo").accessToken
|
||||
},
|
||||
filePath: file.path, // 要上传文件资源的路径
|
||||
name: 'file',
|
||||
success: (res) => {
|
||||
let data = JSON.parse(res.data);
|
||||
if (data.code != 200) {
|
||||
uni.showToast({
|
||||
icon: "none",
|
||||
title: data.message,
|
||||
})
|
||||
this.onError && this.onError(file, data);
|
||||
} else {
|
||||
this.onSuccess && this.onSuccess(file, data);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log(err);
|
||||
this.onError && this.onError(file, err);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<view class="loading-box" :style="loadingStyle">
|
||||
<view class="rotate iconfont icon-loading" :style="icontStyle"></view>
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
props: {
|
||||
size: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
iconColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
mask: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
icontStyle() {
|
||||
let style = `font-size:${this.size}rpx;`;
|
||||
if(this.iconColor){
|
||||
style += `color: ${this.iconColor};`
|
||||
}else if(this.mask){
|
||||
style += 'color: #eee;'
|
||||
}
|
||||
return style;
|
||||
},
|
||||
loadingStyle() {
|
||||
return this.mask ? "background: rgba(0, 0, 0, 0.3);" : "";
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.loading-box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rotate {
|
||||
animation: rotate 2s ease-in-out infinite;
|
||||
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(0deg)
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg)
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<view class="long-press-menu none-pointer-events">
|
||||
<view @longpress.prevent.stop="onLongPress($event)" @touchmove="onTouchMove" @touchend="onTouchEnd">
|
||||
<slot></slot>
|
||||
</view>
|
||||
<view v-if="isShowMenu" class="menu-mask" @touchstart="onClose()" @contextmenu.prevent=""></view>
|
||||
<view v-if="isShowMenu" class="menu" :style="menuStyle">
|
||||
<view class="menu-item" v-for="(item) in items" :key="item.key" @click.prevent="onSelectMenu(item)">
|
||||
<text :style="itemStyle(item)"> {{ item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "long-press-menu",
|
||||
data() {
|
||||
return {
|
||||
isShowMenu: false,
|
||||
isTouchMove: false,
|
||||
style: ""
|
||||
}
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onLongPress(e) {
|
||||
if (this.isTouchMove) {
|
||||
// 屏幕移动时不弹出
|
||||
return;
|
||||
}
|
||||
uni.getSystemInfo({
|
||||
success: (res) => {
|
||||
let touches = e.touches[0];
|
||||
let style = "";
|
||||
/* 因 非H5端不兼容 style 属性绑定 Object ,所以拼接字符 */
|
||||
if (touches.clientY > (res.windowHeight / 2)) {
|
||||
style = `bottom:${res.windowHeight - touches.clientY + 20}px;`;
|
||||
} else {
|
||||
style = `top:${touches.clientY + 20}px;`;
|
||||
}
|
||||
if (touches.clientX > (res.windowWidth / 2)) {
|
||||
style += `right:${res.windowWidth - touches.clientX}px;`;
|
||||
} else {
|
||||
style += `left:${touches.clientX}px;`;
|
||||
}
|
||||
this.menuStyle = style;
|
||||
//
|
||||
this.$nextTick(() => {
|
||||
this.isShowMenu = true;
|
||||
});
|
||||
this.menuTouch = touches
|
||||
}
|
||||
})
|
||||
},
|
||||
onTouchMove(e) {
|
||||
this.isTouchMove = true;
|
||||
let touches = e.touches[0];
|
||||
// 屏幕拖动大于50px时,取消菜单
|
||||
if (Math.abs(touches.clientX - this.menuTouch.clientX) > 50 ||
|
||||
Math.abs(touches.clientY - this.menuTouch.clientY) > 50) {
|
||||
this.onClose();
|
||||
}
|
||||
},
|
||||
onTouchEnd() {
|
||||
this.isTouchMove = false;
|
||||
},
|
||||
onSelectMenu(item) {
|
||||
this.$emit("select", item);
|
||||
this.isShowMenu = false;
|
||||
},
|
||||
onClose() {
|
||||
this.isShowMenu = false;
|
||||
},
|
||||
itemStyle(item) {
|
||||
if (item.color) {
|
||||
return `color:${item.color};`
|
||||
}
|
||||
return `color:#000;`;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.long-press-menu {
|
||||
.menu-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: fixed;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background-color: #fff;
|
||||
z-index: 1000;
|
||||
box-shadow: $im-box-shadow-dark;
|
||||
|
||||
.menu-item {
|
||||
height: 28px;
|
||||
min-width: 120rpx;
|
||||
line-height: 28px;
|
||||
font-size: $im-font-size-small;
|
||||
display: flex;
|
||||
padding: 6px 20px;
|
||||
justify-content: flex-start;
|
||||
|
||||
&:hover {
|
||||
background: $im-bg-active;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<view class="im-nav-bar">
|
||||
<!-- #ifdef APP-PLUS -->
|
||||
<view style="height: var(--status-bar-height)"></view>
|
||||
<!-- #endif -->
|
||||
<view class="im-nav-bar-content">
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<view class="back" @click="handleBackClick" v-if="back">
|
||||
<uni-icons type="back" :size="iconFontSize"></uni-icons>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<view class="title" v-if="title">
|
||||
<slot></slot>
|
||||
</view>
|
||||
<view class="btn">
|
||||
<uni-icons class="btn-item" v-if="search" type="search" :size="iconFontSize"
|
||||
@click="$emit('search')"></uni-icons>
|
||||
<uni-icons class="btn-item" v-if="add" type="plusempty" :size="iconFontSize"
|
||||
@click="$emit('add')"></uni-icons>
|
||||
<uni-icons class="btn-item" v-if="more" type="more-filled" :size="iconFontSize"
|
||||
@click="$emit('more')"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "nav-bar",
|
||||
props: {
|
||||
back: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
title: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
search: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
add: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
more: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
iconFontSize: {
|
||||
type: Number,
|
||||
default: 24
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
height() {
|
||||
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleBackClick() {
|
||||
uni.navigateBack({
|
||||
delta: 1
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.im-nav-bar {
|
||||
background: $im-bg-linear;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
color: $im-text-color;
|
||||
border-bottom: 1px solid $im-border-light;
|
||||
font-size: $im-font-size-large;
|
||||
z-index: 99;
|
||||
|
||||
.im-nav-bar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
height: $im-nav-bar-height;
|
||||
|
||||
.title {}
|
||||
|
||||
.back {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
font-size: 22px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.btn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
padding: 12px;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
|
||||
.btn-item {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<scroll-view scroll-y="true" :style="{height: height}" upper-threshold="200" @scrolltolower="onScrollToBottom"
|
||||
scroll-with-animation="true">
|
||||
<view v-for="(item, idx) in showItems" :key="idx">
|
||||
<slot :item="item">
|
||||
</slot>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "virtual-scroller",
|
||||
data() {
|
||||
return {
|
||||
page: 1,
|
||||
isInitEvent: false,
|
||||
lockTip: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
height: {
|
||||
type: String,
|
||||
default: '60vh'
|
||||
},
|
||||
items: {
|
||||
type: Array
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 30
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onScrollToBottom(e) {
|
||||
if (this.showMaxIdx >= this.items.length) {
|
||||
this.showTip();
|
||||
} else {
|
||||
this.page++;
|
||||
}
|
||||
},
|
||||
showTip() {
|
||||
uni.showToast({
|
||||
title: "已滚动至底部",
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showMaxIdx() {
|
||||
return Math.min(this.page * this.size, this.items.length);
|
||||
},
|
||||
showItems() {
|
||||
return this.items.slice(0, this.showMaxIdx);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,64 @@
|
||||
// 颜色
|
||||
$im-color-primary: #3e45d7;
|
||||
$im-color-primary-light-1: mix(#fff, $im-color-primary, 10%);
|
||||
$im-color-primary-light-2: mix(#fff, $im-color-primary, 20%);
|
||||
$im-color-primary-light-3: mix(#fff, $im-color-primary, 30%);
|
||||
$im-color-primary-light-4: mix(#fff, $im-color-primary, 40%);
|
||||
$im-color-primary-light-5: mix(#fff, $im-color-primary, 50%);
|
||||
$im-color-primary-light-6: mix(#fff, $im-color-primary, 60%);
|
||||
$im-color-primary-light-7: mix(#fff, $im-color-primary, 70%);
|
||||
$im-color-primary-light-8: mix(#fff, $im-color-primary, 80%);
|
||||
$im-color-primary-light-9: mix(#fff, $im-color-primary, 90%);
|
||||
$im-color-primary-dark-1: mix(#000, $im-color-primary, 10%);
|
||||
$im-color-primary-dark-2: mix(#000, $im-color-primary, 20%);
|
||||
$im-color-primary-dark-3: mix(#000, $im-color-primary, 30%);
|
||||
$im-color-primary-dark-4: mix(#000, $im-color-primary, 40%);
|
||||
|
||||
$im-color-success: #18bc37;
|
||||
$im-color-warning: #f3a73f;
|
||||
$im-color-danger: #e43d33;
|
||||
$im-color-info: #8f939c;
|
||||
|
||||
// 文字颜色
|
||||
$im-text-color: #000000;
|
||||
$im-text-color-light: #6a6a6a;
|
||||
$im-text-color-lighter: #909399;
|
||||
$im-text-color-lighter-extra: #c7c7c7;
|
||||
|
||||
// 边框颜色
|
||||
$im-border: #F0F0F0;
|
||||
$im-border-light: #EDEDED;
|
||||
$im-border-lighter: #DCDCDC;
|
||||
$im-border-lighter-extra: #B9B9B9;
|
||||
|
||||
// 文字大小
|
||||
$im-font-size: 30rpx;
|
||||
$im-font-size-small: 28rpx;
|
||||
$im-font-size-smaller: 26rpx;
|
||||
$im-font-size-smaller-extra: 24rpx;
|
||||
$im-font-size-large: 32rpx;
|
||||
$im-font-size-larger: 34rpx;
|
||||
$im-font-size-larger-extra: 36rpx;
|
||||
|
||||
// 阴影
|
||||
$im-box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);
|
||||
$im-box-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
$im-box-shadow-lighter: 0px 0px 6px rgba(0, 0, 0, .12);
|
||||
$im-box-shadow-dark: 0px 1px 10px 2px rgba($color: #a5a4a4, $alpha: 0.5);
|
||||
|
||||
// 背景
|
||||
$im-bg: #f8f9ff;
|
||||
$im-bg-linear: linear-gradient(135deg, #f8f9ff 0%, #ffffff 100%);
|
||||
$im-bg-active: #f7f8fc;
|
||||
$im-bg-active-dark: $im-color-primary-light-9;
|
||||
$im-bg-input: #fafafa;
|
||||
|
||||
|
||||
// 标题
|
||||
$im-title-size: 26px;
|
||||
$im-title-size-1: 22px;
|
||||
$im-title-size-2: 18px;
|
||||
|
||||
$font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif;
|
||||
|
||||
$im-nav-bar-height: 50px;
|
||||
@@ -0,0 +1,258 @@
|
||||
/** 原生button样式 **/
|
||||
uni-button {
|
||||
font-size: $im-font-size !important;
|
||||
border-radius: 40rpx !important;
|
||||
}
|
||||
uni-button[type='primary'] {
|
||||
color: #fff !important;
|
||||
background: linear-gradient(135deg, $im-color-primary, lighten($im-color-primary, 15%)) !important;
|
||||
}
|
||||
|
||||
uni-button[type='primary'][plain] {
|
||||
color: $im-color-primary !important;
|
||||
border: 1px solid $im-color-primary;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
uni-button[type='warn'] {
|
||||
color: #fff !important;
|
||||
background: linear-gradient(135deg, $im-color-danger, lighten($im-color-danger, 15%)) !important;
|
||||
}
|
||||
|
||||
uni-button[type='warn'][plain] {
|
||||
color: $im-color-danger !important;
|
||||
border: 1px solid $im-color-danger !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
uni-button[size='mini'] {
|
||||
font-size: $im-font-size-smaller !important;
|
||||
}
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
// wx小程序只有button,没有uni-botton
|
||||
button {
|
||||
font-size: $im-font-size !important;
|
||||
}
|
||||
|
||||
button[type='primary'] {
|
||||
color: #fff !important;
|
||||
background: linear-gradient(135deg, $im-color-primary, lighten($im-color-primary, 15%)) !important;
|
||||
}
|
||||
|
||||
button[type='primary'][plain] {
|
||||
color: $im-color-primary !important;
|
||||
border: 2px solid $im-color-primary;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
button[type='warn'] {
|
||||
color: #fff !important;
|
||||
background: linear-gradient(135deg, $im-color-danger, lighten($im-color-danger, 15%)) !important;
|
||||
}
|
||||
|
||||
button[type='warn'][plain] {
|
||||
color: $im-color-danger !important;
|
||||
border: 1px solid $im-color-danger !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
button[size='mini'] {
|
||||
font-size: $im-font-size-smaller !important;
|
||||
}
|
||||
// #endif
|
||||
|
||||
.button-hover[type='primary'] {
|
||||
color: #fff !important;
|
||||
background-color: $im-color-primary-dark-1 !important;
|
||||
}
|
||||
|
||||
/** uni-ui input激活后边框、图标颜色 **/
|
||||
.uni-easyinput__content.is-focused:not(.is-input-error-border) {
|
||||
border-color: $im-color-primary-light-2 !important;
|
||||
|
||||
.content-clear-icon {
|
||||
color: $im-color-primary-light-2 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/** 底部导航 **/
|
||||
.uni-tabbar-bottom .uni-tabbar {
|
||||
box-shadow: $im-box-shadow;
|
||||
}
|
||||
|
||||
.uni-tabbar-border {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.segmented-control {
|
||||
border-color: $im-color-primary !important;
|
||||
|
||||
.segmented-control__item--button {
|
||||
border-color: $im-color-primary !important;
|
||||
}
|
||||
.segmented-control__item--button--active {
|
||||
background-color: $im-color-primary !important;
|
||||
|
||||
.segmented-control__text{
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
.segmented-control__text{
|
||||
color: $im-color-primary !important;
|
||||
}
|
||||
}
|
||||
|
||||
.uni-radio-input svg{
|
||||
border-color: white !important;
|
||||
background-color: $im-color-primary !important;
|
||||
}
|
||||
|
||||
.uni-radio-input svg {
|
||||
background-color: $im-color-primary !important;
|
||||
border-color: $im-color-primary !important;
|
||||
background-clip: content-box !important;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%) scale(0.7)!important;
|
||||
}
|
||||
|
||||
.uni-radio-input svg path{
|
||||
fill: $im-color-primary !important;
|
||||
}
|
||||
|
||||
.uni-radio-input {
|
||||
background-color: white !important;
|
||||
border-color: $im-color-primary !important;
|
||||
}
|
||||
|
||||
.uni-radio-input-disabled {
|
||||
background-color: rgb(225, 225, 225) !important;
|
||||
border-color: rgb(209, 209, 209) !important;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.uni-section__content-title {
|
||||
font-size: $im-font-size !important;
|
||||
color: $im-text-color-light;
|
||||
}
|
||||
|
||||
.uni-forms-item__label {
|
||||
color: $im-text-color;
|
||||
font-size: $im-font-size !important;
|
||||
}
|
||||
.uni-forms-item {
|
||||
//margin-bottom: 8px !important;
|
||||
}
|
||||
.uni-easyinput__content-input {
|
||||
font-size: $im-font-size !important;
|
||||
}
|
||||
.uni-easyinput__placeholder-class {
|
||||
color: $im-text-color-lighter;
|
||||
font-size: $im-font-size !important;;
|
||||
}
|
||||
.uni-easyinput__content-textarea {
|
||||
font-size: $im-font-size !important;;
|
||||
}
|
||||
.uni-input-input:disabled {
|
||||
color: $im-text-color-light;
|
||||
}
|
||||
|
||||
.uni-forms-item.is-direction-top .uni-forms-item__label {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.uni-data-checklist .checklist-group .checklist-box .checklist-content .checklist-text {
|
||||
font-size: $im-font-size !important;
|
||||
}
|
||||
.uni-card .uni-card__content {
|
||||
color: unset !important;
|
||||
padding: 10px 0 !important;
|
||||
}
|
||||
|
||||
.uni-tag{
|
||||
transform: scale(0.9);
|
||||
margin-left: 5rpx;
|
||||
border-radius: 12rpx !important;
|
||||
font-weight: 600 !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.uni-switch-input-checked {
|
||||
background-color: $im-color-primary-light-1 !important;
|
||||
border-color: $im-color-primary-light-1 !important;
|
||||
}
|
||||
.uni-modal__title {
|
||||
font-size: $im-font-size-larger !important;
|
||||
}
|
||||
.uni-modal__bd {
|
||||
font-size: $im-font-size;
|
||||
}
|
||||
.uni-modal__ft {
|
||||
font-size: $im-font-size;
|
||||
line-height: 90rpx !important;
|
||||
|
||||
.uni-modal__btn_primary {
|
||||
color: $im-color-primary !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.nav-bar {
|
||||
height: 100rpx;
|
||||
padding: 0 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1rpx solid $im-border;
|
||||
|
||||
.nav-search {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nav-add {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.bottom-btn {
|
||||
margin: 40rpx 40rpx;
|
||||
|
||||
uni-button + uni-button {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-large {
|
||||
width: 64rpx !important;
|
||||
height: 64rpx !important;
|
||||
vertical-align: bottom !important;
|
||||
}
|
||||
|
||||
.emoji-normal {
|
||||
width: 54rpx !important;
|
||||
height: 54rpx !important;
|
||||
vertical-align: bottom !important;
|
||||
}
|
||||
|
||||
.emoji-small {
|
||||
width: 36rpx !important;
|
||||
height: 36rpx !important;
|
||||
vertical-align: bottom !important;
|
||||
}
|
||||
|
||||
.none-pointer-events {
|
||||
uni-image img {
|
||||
// 阻止微信默认长按菜单
|
||||
pointer-events: none;
|
||||
-webkit-pointer-events: none;
|
||||
-ms-pointer-events: none;
|
||||
-moz-pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin-block-start: 1em;
|
||||
margin-block-end: 1em;
|
||||
}
|
||||
@@ -7,6 +7,20 @@ import * as enums from './common/enums.js';
|
||||
import * as date from './common/date';
|
||||
import * as socketApi from './common/wssocket';
|
||||
import * as messageType from './common/messageType';
|
||||
// #ifdef H5
|
||||
import * as recorder from './common/recorder-h5';
|
||||
import ImageResize from 'quill-image-resize-mp';
|
||||
import Quill from 'quill';
|
||||
// 以下组件用于兼容部分手机聊天边框无法输入的问题
|
||||
window.Quill = Quill;
|
||||
window.ImageResize = { default: ImageResize };
|
||||
// 调试器
|
||||
// import VConsole from 'vconsole'
|
||||
// new VConsole();
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
import * as recorder from './common/recorder-app';
|
||||
// #endif
|
||||
|
||||
import App from './App.vue';
|
||||
import { createSSRApp } from 'vue';
|
||||
@@ -15,11 +29,46 @@ import { setupPinia } from './sheep/store';
|
||||
import './main.scss';
|
||||
import 'virtual:uno.css';
|
||||
|
||||
import useChatStore from '@/store/chatStore.js';
|
||||
import useFriendStore from '@/store/friendStore.js';
|
||||
import useGroupStore from '@/store/groupStore.js';
|
||||
import useConfigStore from '@/store/configStore.js';
|
||||
import useUserStore from '@/store/userStore.js';
|
||||
|
||||
import barGroup from '@/components/bar/bar-group';
|
||||
import arrowBar from '@/components/bar/arrow-bar';
|
||||
import btnBar from '@/components/bar/btn-bar';
|
||||
import switchBar from '@/components/bar/switch-bar';
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App);
|
||||
|
||||
setupPinia(app);
|
||||
|
||||
app.config.globalProperties.$http = request;
|
||||
app.config.globalProperties.$wsApi = socketApi;
|
||||
app.config.globalProperties.$msgType = messageType;
|
||||
app.config.globalProperties.$emo = emotion;
|
||||
app.config.globalProperties.$url = url;
|
||||
app.config.globalProperties.$str = str;
|
||||
app.config.globalProperties.$enums = enums;
|
||||
app.config.globalProperties.$date = date;
|
||||
app.config.globalProperties.$rc = recorder;
|
||||
|
||||
app.component('bar-group', barGroup);
|
||||
app.component('arrow-bar', arrowBar);
|
||||
app.component('btn-bar', btnBar);
|
||||
app.component('switch-bar', switchBar);
|
||||
|
||||
// 初始化时再挂载store对象
|
||||
app.config.globalProperties.$mountStore = () => {
|
||||
app.config.globalProperties.chatStore = useChatStore();
|
||||
app.config.globalProperties.friendStore = useFriendStore();
|
||||
app.config.globalProperties.groupStore = useGroupStore();
|
||||
app.config.globalProperties.configStore = useConfigStore();
|
||||
app.config.globalProperties.userStore = useUserStore();
|
||||
};
|
||||
|
||||
return {
|
||||
app,
|
||||
};
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/messages/messages",
|
||||
"path": "pages/message/message",
|
||||
"style": {
|
||||
"navigationBarTitleText": "消息",
|
||||
"navigationStyle": "custom"
|
||||
@@ -147,6 +147,30 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "pages/message",
|
||||
"pages": [
|
||||
{
|
||||
"path": "chat-box",
|
||||
"style": {
|
||||
"mp-weixin": {
|
||||
"disableScroll": true
|
||||
},
|
||||
"app-plus": {
|
||||
// adjustPan窗体高度不变,但窗体上推、adjustResize屏幕高度=webview窗体高度+软键盘高度
|
||||
"softinputMode": "adjustResize"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "user-info",
|
||||
"style": {
|
||||
"navigationBarTitleText": "用户信息",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "pages/profile",
|
||||
"pages": [
|
||||
@@ -840,10 +864,10 @@
|
||||
"selectedIconPath": "static/tabbar/moments-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/messages/messages",
|
||||
"pagePath": "pages/message/message",
|
||||
"text": "消息",
|
||||
"iconPath": "static/tabbar/messages.png",
|
||||
"selectedIconPath": "static/tabbar/messages-active.png"
|
||||
"iconPath": "static/tabbar/message.png",
|
||||
"selectedIconPath": "static/tabbar/message-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/profile/profile",
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<web-view class="page chat-group-video" id="chat-video-wv" @message="onMessage" :src="url"></web-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UNI_APP from '@/.env.js'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
url: "",
|
||||
wv: '',
|
||||
isHost: false,
|
||||
groupId: null,
|
||||
inviterId: null,
|
||||
userInfos: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onMessage(e) {
|
||||
this.onWebviewMessage(e.detail.data[0]);
|
||||
},
|
||||
onInsertMessage(msgInfo) {
|
||||
|
||||
},
|
||||
onWebviewMessage(event) {
|
||||
console.log("来自webview的消息:" + JSON.stringify(event))
|
||||
switch (event.key) {
|
||||
case "WV_READY":
|
||||
this.initWebView();
|
||||
break;
|
||||
case "WV_CLOSE":
|
||||
uni.navigateBack();
|
||||
break;
|
||||
case "INSERT_MESSAGE":
|
||||
this.onInsertMessage(event.data);
|
||||
break;
|
||||
}
|
||||
},
|
||||
sendMessageToWebView(key, message) {
|
||||
// 如果webview还没初始化好,则延迟100ms再推送
|
||||
if (!this.wv) {
|
||||
setTimeout(() => this.sendMessageToWebView(key, message), 100)
|
||||
return;
|
||||
}
|
||||
let event = {
|
||||
key: key,
|
||||
data: message
|
||||
}
|
||||
// #ifdef APP-PLUS
|
||||
this.wv.evalJS(`onEvent('${encodeURIComponent(JSON.stringify(event))}')`)
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
this.wv.postMessage(event, '*');
|
||||
// #endif
|
||||
},
|
||||
initWebView() {
|
||||
// #ifdef APP-PLUS
|
||||
// APP的webview
|
||||
this.wv = this.$scope.$getAppWebview().children()[0]
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
// H5的webview就是iframe
|
||||
this.wv = document.getElementById('chat-video-wv').contentWindow
|
||||
// #endif
|
||||
},
|
||||
initUrl() {
|
||||
this.url = "/hybrid/html/rtc-group/index.html?";
|
||||
this.url += "baseUrl=" + UNI_APP.BASE_URL;
|
||||
this.url += "&groupId=" + this.groupId;
|
||||
this.url += "&userId=" + this.userStore.userInfo.id;
|
||||
this.url += "&inviterId=" + this.inviterId;
|
||||
this.url += "&isHost=" + this.isHost;
|
||||
this.url += "&loginInfo=" + JSON.stringify(uni.getStorageSync("loginInfo"));
|
||||
this.url += "&userInfos=" + JSON.stringify(this.userInfos);
|
||||
this.url += "&config=" + JSON.stringify(this.configStore.webrtc);
|
||||
},
|
||||
},
|
||||
onBackPress() {
|
||||
this.sendMessageToWebView("NAV_BACK", {})
|
||||
},
|
||||
onLoad(options) {
|
||||
uni.$on('WS_RTC_GROUP', msg => {
|
||||
// 推送给web-view处理
|
||||
this.sendMessageToWebView("RTC_MESSAGE", msg);
|
||||
})
|
||||
// #ifdef H5
|
||||
window.onmessage = (e) => {
|
||||
this.onWebviewMessage(e.data.data.arg);
|
||||
}
|
||||
// #endif
|
||||
// 是否发起人
|
||||
this.isHost = JSON.parse(options.isHost);
|
||||
// 发起者的用户
|
||||
this.inviterId = options.inviterId;
|
||||
// 解析页面跳转时带过来的好友信息
|
||||
this.groupId = options.groupId;
|
||||
// 邀请的用户信息
|
||||
this.userInfos = JSON.parse(decodeURIComponent(options.userInfos));
|
||||
|
||||
// 构建url
|
||||
this.initUrl();
|
||||
},
|
||||
onUnload() {
|
||||
uni.$off('WS_RTC_GROUP')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chat-group-video {
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 60rpx;
|
||||
padding: 5px;
|
||||
background-color: white;
|
||||
line-height: 50px;
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
|
||||
.btn-side {
|
||||
position: absolute;
|
||||
line-height: 60rpx;
|
||||
font-size: 28rpx;
|
||||
cursor: pointer;
|
||||
|
||||
&.left {
|
||||
left: 30rpx;
|
||||
}
|
||||
|
||||
&.right {
|
||||
right: 30rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<web-view class="page chat-private-video" id="chat-video-wv" @message="onMessage" :src="url"></web-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UNI_APP from '@/.env.js'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
url: "",
|
||||
wv: '',
|
||||
mode: "video",
|
||||
isHost: true,
|
||||
friend: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onMessage(e) {
|
||||
this.onWebviewMessage(e.detail.data[0]);
|
||||
},
|
||||
onWebviewMessage(event) {
|
||||
console.log("来自webview的消息:" + JSON.stringify(event))
|
||||
switch (event.key) {
|
||||
case "WV_READY":
|
||||
this.initWebView();
|
||||
break;
|
||||
case "WV_CLOSE":
|
||||
uni.navigateBack();
|
||||
break;
|
||||
}
|
||||
},
|
||||
sendMessageToWebView(key, message) {
|
||||
// 如果webview还没初始化好,则延迟100ms再推送
|
||||
if (!this.wv) {
|
||||
setTimeout(() => this.sendMessageToWebView(key, message), 100)
|
||||
return;
|
||||
}
|
||||
let event = {
|
||||
key: key,
|
||||
data: message
|
||||
}
|
||||
// #ifdef APP-PLUS
|
||||
this.wv.evalJS(`onEvent('${encodeURIComponent(JSON.stringify(event))}')`)
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
this.wv.postMessage(event, '*');
|
||||
// #endif
|
||||
},
|
||||
initWebView() {
|
||||
// #ifdef APP-PLUS
|
||||
// APP的webview
|
||||
this.wv = this.$scope.$getAppWebview().children()[0]
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
// H5的webview就是iframe
|
||||
this.wv = document.getElementById('chat-video-wv').contentWindow
|
||||
// #endif
|
||||
},
|
||||
initUrl() {
|
||||
this.url = "/hybrid/html/rtc-private/index.html";
|
||||
this.url += "?mode=" + this.mode;
|
||||
this.url += "&isHost=" + this.isHost;
|
||||
this.url += "&baseUrl=" + UNI_APP.BASE_URL;
|
||||
this.url += "&loginInfo=" + JSON.stringify(uni.getStorageSync("loginInfo"));
|
||||
this.url += "&userInfo=" + JSON.stringify(this.userStore.userInfo);
|
||||
this.url += "&friend=" + JSON.stringify(this.friend);
|
||||
this.url += "&config=" + JSON.stringify(this.configStore.webrtc);
|
||||
},
|
||||
},
|
||||
onBackPress() {
|
||||
this.sendMessageToWebView("NAV_BACK", {})
|
||||
},
|
||||
onLoad(options) {
|
||||
uni.$on('WS_RTC_PRIVATE', msg => {
|
||||
// 推送给web-view处理
|
||||
this.sendMessageToWebView("RTC_MESSAGE", msg);
|
||||
})
|
||||
// #ifdef H5
|
||||
window.onmessage = (e) => {
|
||||
this.onWebviewMessage(e.data.data.arg);
|
||||
}
|
||||
// #endif
|
||||
// 模式:视频呼叫/语音呼叫
|
||||
this.mode = options.mode;
|
||||
// 是否呼叫方
|
||||
this.isHost = JSON.parse(options.isHost);
|
||||
// 解析页面跳转时带过来的好友信息
|
||||
this.friend = JSON.parse(decodeURIComponent(options.friend));
|
||||
// 构建url
|
||||
this.initUrl();
|
||||
},
|
||||
onUnload() {
|
||||
uni.$off('WS_RTC_PRIVATE')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chat-web-view {}
|
||||
</style>
|
||||
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<view class="tab-page">
|
||||
<nav-bar search @search="onSearch()">消息</nav-bar>
|
||||
<view v-if="loading" class="chat-loading">
|
||||
<loading :size="50" :mask="false">
|
||||
<view>消息接收中...</view>
|
||||
</loading>
|
||||
</view>
|
||||
<view v-else-if="initializing" class="chat-loading">
|
||||
<loading :size="50" :mask="false">
|
||||
<view>正在初始化...</view>
|
||||
</loading>
|
||||
</view>
|
||||
<view class="nav-bar" v-if="showSearch">
|
||||
<view class="nav-search">
|
||||
<uni-search-bar focus="true" radius="100" v-model="searchText" cancelButton="none"
|
||||
placeholder="搜索"></uni-search-bar>
|
||||
</view>
|
||||
</view>
|
||||
<view class="chat-tip" v-if="!initializing && !loading && chatStore.chats.length == 0">
|
||||
<view class="tip-icon">
|
||||
<text class="iconfont icon-chat"></text>
|
||||
</view>
|
||||
<view class="tip-title">还没有聊天</view>
|
||||
<view class="tip-content">添加好友或创建群聊,开始精彩的对话吧</view>
|
||||
</view>
|
||||
<scroll-view class="scroll-bar" v-else scroll-with-animation="true" scroll-y="true">
|
||||
<view v-for="(chat, index) in chatStore.chats" :key="index">
|
||||
<long-press-menu v-if="isShowChat(chat)" :items="menu.items" @select="onSelectMenu($event, index)">
|
||||
<chat-item :chat="chat" :index="index" :active="menu.chatIdx == index"></chat-item>
|
||||
</long-press-menu>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
showSearch: false,
|
||||
searchText: "",
|
||||
menu: {
|
||||
show: false,
|
||||
style: "",
|
||||
chatIdx: -1,
|
||||
isTouchMove: false,
|
||||
items: [{
|
||||
key: 'DELETE',
|
||||
name: '删除该聊天',
|
||||
icon: 'trash',
|
||||
color: '#e64e4e'
|
||||
},
|
||||
{
|
||||
key: 'TOP',
|
||||
name: '置顶该聊天',
|
||||
icon: 'arrow-up'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSelectMenu(item, chatIdx) {
|
||||
switch (item.key) {
|
||||
case 'DELETE':
|
||||
this.removeChat(chatIdx);
|
||||
break;
|
||||
case 'TOP':
|
||||
this.moveToTop(chatIdx);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
this.menu.show = false;
|
||||
},
|
||||
removeChat(chatIdx) {
|
||||
this.chatStore.removeChat(chatIdx);
|
||||
},
|
||||
moveToTop(chatIdx) {
|
||||
this.chatStore.moveTop(chatIdx);
|
||||
},
|
||||
isShowChat(chat) {
|
||||
if (chat.delete) {
|
||||
return false;
|
||||
}
|
||||
return !this.searchText || chat.showName.includes(this.searchText)
|
||||
},
|
||||
onSearch() {
|
||||
this.showSearch = !this.showSearch;
|
||||
this.searchText = "";
|
||||
},
|
||||
refreshUnreadBadge() {
|
||||
if (this.unreadCount > 0) {
|
||||
uni.setTabBarBadge({
|
||||
index: 0,
|
||||
text: this.unreadCount + ""
|
||||
})
|
||||
} else {
|
||||
uni.removeTabBarBadge({
|
||||
index: 0,
|
||||
complete: () => {}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
unreadCount() {
|
||||
let count = 0;
|
||||
this.chatStore.chats.forEach(chat => {
|
||||
if (!chat.isDnd && !chat.delete) {
|
||||
count += chat.unreadCount;
|
||||
}
|
||||
})
|
||||
return count;
|
||||
},
|
||||
loading() {
|
||||
return this.chatStore.loading;
|
||||
},
|
||||
initializing() {
|
||||
return !this.configStore.appInit;
|
||||
},
|
||||
showChats() {
|
||||
this.chatStore.chats.filter((chat) => !chat.delete && chat.showName && chat.showName.includes(this
|
||||
.searchText))
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
unreadCount(newCount, oldCount) {
|
||||
this.refreshUnreadBadge();
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.refreshUnreadBadge();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.tab-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.chat-tip {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 40rpx;
|
||||
text-align: center;
|
||||
width: 80%;
|
||||
|
||||
.tip-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 40rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
border: 1rpx solid $im-bg-active;
|
||||
|
||||
.iconfont {
|
||||
font-size: 60rpx;
|
||||
color: $im-text-color-lighter;
|
||||
}
|
||||
}
|
||||
|
||||
.tip-title {
|
||||
font-size: $im-font-size-large;
|
||||
color: $im-text-color;
|
||||
font-weight: 500;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.tip-content {
|
||||
font-size: $im-font-size-smaller;
|
||||
color: $im-text-color-lighter;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 50rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-loading {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 120rpx;
|
||||
background: white;
|
||||
color: $im-text-color-lighter;
|
||||
|
||||
.loading-box {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-bar {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,252 @@
|
||||
<template>
|
||||
<view class="page user-info">
|
||||
<nav-bar back>用户信息</nav-bar>
|
||||
<uni-card :is-shadow="false" is-full :border="false">
|
||||
<view class="content">
|
||||
<head-image
|
||||
:name="userInfo.nickName"
|
||||
:url="userInfo.headImageThumb"
|
||||
:size="160"
|
||||
@click="onShowFullImage()"
|
||||
></head-image>
|
||||
|
||||
<view class="info-item">
|
||||
<view class="info-primary">
|
||||
<text class="info-username">{{ userInfo.nickName }}</text>
|
||||
<text v-show="userInfo.sex == 0" class="iconfont icon-man" color="darkblue"></text>
|
||||
<text v-show="userInfo.sex == 1" class="iconfont icon-girl" color="darkred"></text>
|
||||
</view>
|
||||
<view class="info-text">
|
||||
<text class="label-text">用户名:</text>
|
||||
<text class="content-text"> {{ userInfo.userName }}</text>
|
||||
</view>
|
||||
<view class="info-text">
|
||||
<view>
|
||||
<text class="label-text">签名:</text>
|
||||
<text class="content-text"> {{ userInfo.signature }} </text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</uni-card>
|
||||
<bar-group v-if="isFriend">
|
||||
<switch-bar title="消息免打扰" :checked="friendInfo.isDnd" @change="onDndChange"></switch-bar>
|
||||
</bar-group>
|
||||
<bar-group v-if="chatIdx >= 0">
|
||||
<arrow-bar title="清空聊天记录" @tap="onCleanMessage()"></arrow-bar>
|
||||
</bar-group>
|
||||
<bar-group>
|
||||
<btn-bar v-show="isFriend" type="primary" title="发送消息" @tap="onSendMessage()"> </btn-bar>
|
||||
<btn-bar v-show="!isFriend" type="primary" title="加为好友" @tap="onAddFriend()"></btn-bar>
|
||||
<btn-bar v-show="isFriend" type="danger" title="删除好友" @tap="onDelFriend()"></btn-bar>
|
||||
</bar-group>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
userInfo: {},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onShowFullImage() {
|
||||
let imageUrl = this.userInfo.headImage;
|
||||
if (imageUrl) {
|
||||
uni.previewImage({
|
||||
urls: [imageUrl],
|
||||
});
|
||||
}
|
||||
},
|
||||
onSendMessage() {
|
||||
let chat = {
|
||||
type: 'PRIVATE',
|
||||
targetId: this.userInfo.id,
|
||||
showName: this.userInfo.nickName,
|
||||
headImage: this.userInfo.headImageThumb,
|
||||
};
|
||||
if (this.isFriend) {
|
||||
chat.isDnd = this.friendInfo.isDnd;
|
||||
}
|
||||
this.chatStore.openChat(chat);
|
||||
let chatIdx = this.chatStore.findChatIdx(chat);
|
||||
uni.navigateTo({
|
||||
url: '/pages/message/chat-box?chatIdx=' + chatIdx,
|
||||
});
|
||||
},
|
||||
onAddFriend() {
|
||||
this.$http({
|
||||
url: '/friend/add?friendId=' + this.userInfo.id,
|
||||
method: 'POST',
|
||||
}).then((data) => {
|
||||
let friend = {
|
||||
id: this.userInfo.id,
|
||||
nickName: this.userInfo.nickName,
|
||||
headImage: this.userInfo.headImageThumb,
|
||||
online: this.userInfo.online,
|
||||
deleted: false,
|
||||
};
|
||||
this.friendStore.addFriend(friend);
|
||||
uni.showToast({
|
||||
title: '对方已成为您的好友',
|
||||
icon: 'none',
|
||||
});
|
||||
});
|
||||
},
|
||||
onDelFriend() {
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: `确认删除 '${this.userInfo.nickName}',并删除聊天记录吗?`,
|
||||
success: (res) => {
|
||||
if (res.cancel) return;
|
||||
this.$http({
|
||||
url: `/friend/delete/${this.userInfo.id}`,
|
||||
method: 'delete',
|
||||
}).then((data) => {
|
||||
this.friendStore.removeFriend(this.userInfo.id);
|
||||
this.chatStore.removePrivateChat(this.userInfo.id);
|
||||
uni.showToast({
|
||||
title: `与 '${this.userInfo.nickName}'的好友关系已解除`,
|
||||
icon: 'none',
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
onCleanMessage() {
|
||||
uni.showModal({
|
||||
title: '清空聊天记录',
|
||||
content: `确认删除与'${this.userInfo.nickName}'的聊天记录吗?`,
|
||||
confirmText: '确认',
|
||||
success: (res) => {
|
||||
if (res.cancel) return;
|
||||
this.chatStore.cleanMessage(this.chatIdx);
|
||||
uni.showToast({
|
||||
title: `您清空了'${this.userInfo.nickName}'的聊天记录`,
|
||||
icon: 'none',
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
onDndChange(e) {
|
||||
let isDnd = e.detail.value;
|
||||
let friendId = this.userInfo.id;
|
||||
let formData = {
|
||||
friendId: friendId,
|
||||
isDnd: isDnd,
|
||||
};
|
||||
this.$http({
|
||||
url: '/friend/dnd',
|
||||
method: 'PUT',
|
||||
data: formData,
|
||||
}).then(() => {
|
||||
this.friendStore.setDnd(friendId, isDnd);
|
||||
let chat = this.chatStore.findChatByFriend(friendId);
|
||||
if (chat) {
|
||||
this.chatStore.setDnd(chat, isDnd);
|
||||
}
|
||||
});
|
||||
},
|
||||
updateFriendInfo() {
|
||||
if (this.isFriend) {
|
||||
// store的数据不能直接修改,深拷贝一份store的数据
|
||||
let friend = JSON.parse(JSON.stringify(this.friendInfo));
|
||||
friend.headImage = this.userInfo.headImageThumb;
|
||||
friend.nickName = this.userInfo.nickName;
|
||||
|
||||
// 更新好友列表中的昵称和头像
|
||||
this.friendStore.updateFriend(friend);
|
||||
// 更新会话中的头像和昵称
|
||||
this.chatStore.updateChatFromFriend(this.userInfo);
|
||||
}
|
||||
},
|
||||
loadUserInfo(id) {
|
||||
this.$http({
|
||||
url: '/user/find/' + id,
|
||||
method: 'GET',
|
||||
}).then((user) => {
|
||||
this.userInfo = user;
|
||||
// 如果发现好友的头像和昵称改了,进行更新
|
||||
this.updateFriendInfo();
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isFriend() {
|
||||
return this.friendStore.isFriend(this.userInfo.id);
|
||||
},
|
||||
friendInfo() {
|
||||
return this.friendStore.findFriend(this.userInfo.id);
|
||||
},
|
||||
chatIdx() {
|
||||
let chat = this.chatStore.findChatByFriend(this.userInfo.id);
|
||||
if (chat) {
|
||||
return this.chatStore.findChatIdx(chat);
|
||||
}
|
||||
return -1;
|
||||
},
|
||||
},
|
||||
onLoad(options) {
|
||||
// 查询用户信息
|
||||
this.loadUserInfo(options.id);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-info {
|
||||
.content {
|
||||
height: 200rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx;
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
padding-left: 40rpx;
|
||||
flex: 1;
|
||||
|
||||
.info-text {
|
||||
line-height: 1.5;
|
||||
//margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.label-text {
|
||||
font-size: $im-font-size-small;
|
||||
color: $im-text-color-light;
|
||||
}
|
||||
|
||||
.content-text {
|
||||
font-size: $im-font-size-small;
|
||||
color: $im-text-color-light;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
.info-primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10rpx;
|
||||
|
||||
.info-username {
|
||||
font-size: $im-font-size-large;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.icon-man {
|
||||
color: $im-text-color;
|
||||
font-size: $im-font-size-large;
|
||||
padding-left: 10rpx;
|
||||
}
|
||||
|
||||
.icon-girl {
|
||||
color: darkred;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,65 +0,0 @@
|
||||
<template>
|
||||
<wd-config-provider theme="dark">
|
||||
<wd-navbar title="" safeAreaInsetTop style="z-index: 999999 !important" :bordered="false">
|
||||
<template #left>
|
||||
<view class="flex items-center text-base">
|
||||
<text class="mr-4">消息</text>
|
||||
<text>缘友</text>
|
||||
</view>
|
||||
</template>
|
||||
<template #right>
|
||||
<navigator url="/pages/" open-type="navigate" hover-class="navigator-hover">
|
||||
<view class="flex items-center rounded-full h-44rpx bg-color-theme py-1 px-2 text-black">
|
||||
<uni-icons type="chat" size="20" color="#333" />
|
||||
<text class="text-sm c-black">互动消息</text>
|
||||
</view>
|
||||
</navigator>
|
||||
</template>
|
||||
</wd-navbar>
|
||||
|
||||
<scroll-view style="height: calc(100vh - 44px - 50px)" scroll-y>
|
||||
<view
|
||||
class="flex items-center border-1 border-b-solid border-#333333 px-6 py-2"
|
||||
v-for="(message, index) in 16"
|
||||
:key="index"
|
||||
>
|
||||
<wd-badge :modelValue="message.unread || 1" :top="10" :right="10">
|
||||
<image
|
||||
:src="message.avatar"
|
||||
class="mr-2 size-120rpx rounded-full bg-#333333"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</wd-badge>
|
||||
<view class="flex-1 overflow-hidden px-3">
|
||||
<view class="flex items-center justify-between">
|
||||
<text class="text-base">{{ message.username || '发放' }}</text>
|
||||
<text class="text-xs c-gray">{{ message.time || 'ww' }}</text>
|
||||
</view>
|
||||
<view class="flex">
|
||||
<text class="mt-1 flex-1 text-xs truncate c-gray">
|
||||
{{ message.lastMessage || '啊发发看来福娃' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="h-100rpx"></view>
|
||||
</scroll-view>
|
||||
</wd-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
interface Message {
|
||||
id: number;
|
||||
username: string;
|
||||
avatar: string;
|
||||
lastMessage: string;
|
||||
time: string;
|
||||
unread: number;
|
||||
}
|
||||
|
||||
const unreadCount = computed(() => {
|
||||
return messageList.value.reduce((total, message) => total + message.unread, 0);
|
||||
});
|
||||
</script>
|
||||
@@ -1,65 +0,0 @@
|
||||
<template>
|
||||
<wd-config-provider theme="dark">
|
||||
<wd-navbar title="" safeAreaInsetTop style="z-index: 999999 !important" :bordered="false">
|
||||
<template #left>
|
||||
<view class="flex items-center text-base">
|
||||
<text class="mr-4">消息</text>
|
||||
<text>缘友</text>
|
||||
</view>
|
||||
</template>
|
||||
<template #right>
|
||||
<navigator url="/pages/" open-type="navigate" hover-class="navigator-hover">
|
||||
<view class="flex items-center rounded-full h-44rpx bg-color-theme py-1 px-2 text-black">
|
||||
<uni-icons type="chat" size="20" color="#333" />
|
||||
<text class="text-sm c-black">互动消息</text>
|
||||
</view>
|
||||
</navigator>
|
||||
</template>
|
||||
</wd-navbar>
|
||||
|
||||
<scroll-view style="height: calc(100vh - 44px - 50px)" scroll-y>
|
||||
<view
|
||||
class="flex items-center border-1 border-b-solid border-#333333 px-6 py-2"
|
||||
v-for="(message, index) in 16"
|
||||
:key="index"
|
||||
>
|
||||
<wd-badge :modelValue="message.unread || 1" :top="10" :right="10">
|
||||
<image
|
||||
:src="message.avatar"
|
||||
class="mr-2 size-120rpx rounded-full bg-#333333"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</wd-badge>
|
||||
<view class="flex-1 overflow-hidden px-3">
|
||||
<view class="flex items-center justify-between">
|
||||
<text class="text-base">{{ message.username || '发放' }}</text>
|
||||
<text class="text-xs c-gray">{{ message.time || 'ww' }}</text>
|
||||
</view>
|
||||
<view class="flex">
|
||||
<text class="mt-1 flex-1 text-xs truncate c-gray">
|
||||
{{ message.lastMessage || '啊发发看来福娃' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="h-100rpx"></view>
|
||||
</scroll-view>
|
||||
</wd-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
interface Message {
|
||||
id: number;
|
||||
username: string;
|
||||
avatar: string;
|
||||
lastMessage: string;
|
||||
time: string;
|
||||
unread: number;
|
||||
}
|
||||
|
||||
const unreadCount = computed(() => {
|
||||
return messageList.value.reduce((total, message) => total + message.unread, 0);
|
||||
});
|
||||
</script>
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 20 KiB |