xw hai 3 semanas
pai
achega
84388b6a2a
Modificáronse 100 ficheiros con 9621 adicións e 202 borrados
  1. 9 0
      .env.development
  2. 7 1
      babel.config.js
  3. 6 1
      package.json
  4. 1911 0
      public/BenzAMRRecorder.js
  5. 1 0
      public/index.html
  6. BIN=BIN
      public/openIM.wasm
  7. BIN=BIN
      public/sql-wasm.wasm
  8. 561 0
      public/wasm_exec.js
  9. 0 0
      public/worker-legacy.js
  10. 0 0
      public/worker.js
  11. 9 0
      src/api/company/companyUser.js
  12. 16 0
      src/api/company/friend.js
  13. 68 0
      src/api/coupon/coupon.js
  14. 18 0
      src/api/group.js
  15. 1 0
      src/assets/audio/beCalled.mp3
  16. 3055 0
      src/assets/css/animate.css
  17. 48 0
      src/assets/doctor.svg
  18. 14 0
      src/assets/guanjia.svg
  19. 3 0
      src/assets/icon/iconfont.css
  20. BIN=BIN
      src/assets/icon/iconfont.eot
  21. 0 0
      src/assets/icon/iconfont.js
  22. 28 0
      src/assets/icon/iconfont.svg
  23. BIN=BIN
      src/assets/icon/iconfont.ttf
  24. BIN=BIN
      src/assets/icon/iconfont.woff
  25. BIN=BIN
      src/assets/icon/iconfont.woff2
  26. 51 0
      src/assets/icon/tim.css
  27. BIN=BIN
      src/assets/icon/tim.eot
  28. 18 0
      src/assets/icon/tim.svg
  29. BIN=BIN
      src/assets/icon/tim.ttf
  30. BIN=BIN
      src/assets/icon/tim.woff
  31. 1 0
      src/assets/image/add-friend.png
  32. 1 0
      src/assets/image/back.png
  33. 1 0
      src/assets/image/big-camera-off.png
  34. 1 0
      src/assets/image/big-camera-on.png
  35. 1 0
      src/assets/image/big-mic-off.png
  36. 1 0
      src/assets/image/big-mic-on.png
  37. 1 0
      src/assets/image/camera-open.png
  38. 1 0
      src/assets/image/close-mic.png
  39. 1 0
      src/assets/image/close-relay.png
  40. 1 0
      src/assets/image/close.png
  41. 1 0
      src/assets/image/hangup.png
  42. 1 0
      src/assets/image/live-icon-gray.png
  43. 1 0
      src/assets/image/live-icon-hover.png
  44. 1 0
      src/assets/image/live-icon.png
  45. 1 0
      src/assets/image/living-icon.gif
  46. 1 0
      src/assets/image/logo.png
  47. 1 0
      src/assets/image/merger-relay.png
  48. 1 0
      src/assets/image/no-video.png
  49. 1 0
      src/assets/image/open-mic.png
  50. 1 0
      src/assets/image/pause-icon.png
  51. 1 0
      src/assets/image/poster.png
  52. 1 0
      src/assets/image/sig-relay.png
  53. 1 0
      src/assets/image/video-bg.png
  54. 1 0
      src/assets/image/web-pusher-start.png
  55. 1 0
      src/assets/image/web-pusher-stop.png
  56. 56 0
      src/assets/qunliao.svg
  57. 78 0
      src/components/ImAvatar/index.vue
  58. 83 0
      src/components/blacklist/blacklist-item.vue
  59. 44 0
      src/components/blacklist/blacklist.vue
  60. 74 39
      src/components/conversation/conversation-item.vue
  61. 5 3
      src/components/conversation/conversation-list.vue
  62. 1 1
      src/components/conversation/conversation-selected-list.vue
  63. 1 1
      src/components/conversation/conversationProfile/add-friend-profile
  64. 1 1
      src/components/conversation/conversationProfile/group-member-list.vue
  65. 1 1
      src/components/conversation/conversationProfile/group-profile.vue
  66. 1 1
      src/components/conversation/conversationProfile/user-profile.vue
  67. 134 41
      src/components/conversation/current-conversation.vue
  68. 3 3
      src/components/friend/friend-application/application-item.vue
  69. 12 3
      src/components/friend/friend-container.vue
  70. 2 2
      src/components/friend/friend-item.vue
  71. 9 9
      src/components/friend/friend-list.vue
  72. 444 0
      src/components/group-live/components/live-chat.vue
  73. 177 0
      src/components/group-live/components/live-gift.vue
  74. 201 0
      src/components/group-live/components/live-header.vue
  75. 331 0
      src/components/group-live/components/live-player.vue
  76. 378 0
      src/components/group-live/components/live-pusher.vue
  77. 113 0
      src/components/group-live/components/live-share.vue
  78. 47 0
      src/components/group-live/components/qrcode.vue
  79. 90 0
      src/components/group-live/index.vue
  80. 234 0
      src/components/group/create-group.vue
  81. 167 0
      src/components/group/group-content.vue
  82. 98 0
      src/components/group/group-item.vue
  83. 214 0
      src/components/group/group-list.vue
  84. 167 0
      src/components/group/member-profile-card.vue
  85. 6 4
      src/components/layout/side-bar.vue
  86. 1 1
      src/components/message/merger-message/message-merger.vue
  87. 48 20
      src/components/message/message-bubble.vue
  88. 10 2
      src/components/message/message-elements/image-element.vue
  89. 14 1
      src/components/message/message-elements/video-element.vue
  90. 2 2
      src/components/message/message-footer.vue
  91. 3 2
      src/components/message/message-header.vue
  92. 15 11
      src/components/message/message-item.vue
  93. 76 44
      src/components/message/message-send-box.vue
  94. 2 2
      src/components/message/trtc-calling/group-member-list.vue
  95. 145 0
      src/components/my-profile.vue
  96. 79 0
      src/components/profile-card.vue
  97. 80 0
      src/components/qr-code-list.vue
  98. 5 6
      src/components/user/login.vue
  99. 4 0
      src/constant/call.js
  100. 116 0
      src/layout/index.vue

+ 9 - 0
.env.development

@@ -39,6 +39,15 @@ ENV = 'development'
 # FS管理系统/开发环境
 VUE_APP_BASE_API = '/dev-api'
 
+# 本地调试 IM 页面 UI:本地后端签发的 token 与线上 OpenIM 不一致会 1507。
+# 做法:新建 .env.development.local(勿提交仓库),把 dev 代理指到与线上一致的后端,再用该环境账号登录:
+# VUE_APP_DEV_PROXY_TARGET=https://你的测试或生产网关域名(与 npm 里线上一致,无末尾路径)
+# 代理到线上时会把 VUE_APP_BASE_API(如 /dev-api)改写成线上接口前缀,默认 /prod-api(与 .env.production 一致);若线上前缀不同可设:
+# VUE_APP_DEV_PROXY_REMOTE_PREFIX=/你的线上-api前缀
+# 可选:IM 网关非默认时覆盖
+# VUE_APP_OPENIM_API_ADDR=https://webim.moonxiang.com/api
+# VUE_APP_OPENIM_WS_ADDR=wss://webim.moonxiang.com/msg_gateway
+
 #默认 1、会员 2、企微
 VUE_APP_COURSE_DEFAULT = 1
 

+ 7 - 1
babel.config.js

@@ -3,11 +3,17 @@ module.exports = {
     // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app
     '@vue/cli-plugin-babel/preset'
   ],
+  plugins: [
+    '@babel/plugin-syntax-import-meta'
+  ],
   'env': {
     'development': {
       // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require().
       // This plugin can significantly increase the speed of hot updates, when you have a large number of pages.
-      'plugins': ['dynamic-import-node']
+      'plugins': [
+        'dynamic-import-node',
+        '@babel/plugin-syntax-import-meta'
+      ]
     }
   }
 }

+ 6 - 1
package.json

@@ -88,6 +88,7 @@
   "dependencies": {
     "@amap/amap-jsapi-loader": "^1.0.1",
     "@jridgewell/gen-mapping": "^0.3.5",
+    "@openim/wasm-client-sdk": "^3.8.3-patch.9",
     "@riophae/vue-treeselect": "0.4.0",
     "axios": "0.18.1",
     "chart.js": "^2.9.4",
@@ -106,6 +107,7 @@
     "js-beautify": "1.10.2",
     "js-cookie": "2.2.0",
     "jsencrypt": "3.0.0-rc.1",
+    "livekit-client": "^2.18.4",
     "lodash.clonedeep": "^4.5.0",
     "lodash.merge": "^4.6.2",
     "moment": "^2.29.4",
@@ -139,6 +141,7 @@
     "wangeditor": "^4.6.13"
   },
   "devDependencies": {
+    "@babel/plugin-syntax-import-meta": "^7.10.4",
     "@vue/cli-plugin-babel": "4.4.4",
     "@vue/cli-plugin-eslint": "4.4.4",
     "@vue/cli-plugin-unit-jest": "4.4.4",
@@ -164,9 +167,11 @@
     "script-ext-html-webpack-plugin": "2.1.3",
     "script-loader": "0.7.2",
     "serve-static": "1.13.2",
+    "string-replace-loader": "^2.3.0",
     "svg-sprite-loader": "4.1.3",
     "svgo": "1.2.0",
-    "vue-template-compiler": "2.6.10"
+    "vue-template-compiler": "2.6.10",
+    "worker-plugin": "^5.0.1"
   },
   "engines": {
     "node": ">=8.9",

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1911 - 0
public/BenzAMRRecorder.js


+ 1 - 0
public/index.html

@@ -1,6 +1,7 @@
 <!DOCTYPE html>
 <html>
   <head>
+    <script src="/wasm_exec.js"></script>
     <meta charset="utf-8">
     <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
     <meta name="renderer" content="webkit">

BIN=BIN
public/openIM.wasm


BIN=BIN
public/sql-wasm.wasm


+ 561 - 0
public/wasm_exec.js

@@ -0,0 +1,561 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+"use strict";
+
+(() => {
+	const enosys = () => {
+		const err = new Error("not implemented");
+		err.code = "ENOSYS";
+		return err;
+	};
+
+	if (!globalThis.fs) {
+		let outputBuf = "";
+		globalThis.fs = {
+			constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
+			writeSync(fd, buf) {
+				outputBuf += decoder.decode(buf);
+				const nl = outputBuf.lastIndexOf("\n");
+				if (nl != -1) {
+					console.log(outputBuf.substring(0, nl));
+					outputBuf = outputBuf.substring(nl + 1);
+				}
+				return buf.length;
+			},
+			write(fd, buf, offset, length, position, callback) {
+				if (offset !== 0 || length !== buf.length || position !== null) {
+					callback(enosys());
+					return;
+				}
+				const n = this.writeSync(fd, buf);
+				callback(null, n);
+			},
+			chmod(path, mode, callback) { callback(enosys()); },
+			chown(path, uid, gid, callback) { callback(enosys()); },
+			close(fd, callback) { callback(enosys()); },
+			fchmod(fd, mode, callback) { callback(enosys()); },
+			fchown(fd, uid, gid, callback) { callback(enosys()); },
+			fstat(fd, callback) { callback(enosys()); },
+			fsync(fd, callback) { callback(null); },
+			ftruncate(fd, length, callback) { callback(enosys()); },
+			lchown(path, uid, gid, callback) { callback(enosys()); },
+			link(path, link, callback) { callback(enosys()); },
+			lstat(path, callback) { callback(enosys()); },
+			mkdir(path, perm, callback) { callback(enosys()); },
+			open(path, flags, mode, callback) { callback(enosys()); },
+			read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
+			readdir(path, callback) { callback(enosys()); },
+			readlink(path, callback) { callback(enosys()); },
+			rename(from, to, callback) { callback(enosys()); },
+			rmdir(path, callback) { callback(enosys()); },
+			stat(path, callback) { callback(enosys()); },
+			symlink(path, link, callback) { callback(enosys()); },
+			truncate(path, length, callback) { callback(enosys()); },
+			unlink(path, callback) { callback(enosys()); },
+			utimes(path, atime, mtime, callback) { callback(enosys()); },
+		};
+	}
+
+	if (!globalThis.process) {
+		globalThis.process = {
+			getuid() { return -1; },
+			getgid() { return -1; },
+			geteuid() { return -1; },
+			getegid() { return -1; },
+			getgroups() { throw enosys(); },
+			pid: -1,
+			ppid: -1,
+			umask() { throw enosys(); },
+			cwd() { throw enosys(); },
+			chdir() { throw enosys(); },
+		}
+	}
+
+	if (!globalThis.crypto) {
+		throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
+	}
+
+	if (!globalThis.performance) {
+		throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
+	}
+
+	if (!globalThis.TextEncoder) {
+		throw new Error("globalThis.TextEncoder is not available, polyfill required");
+	}
+
+	if (!globalThis.TextDecoder) {
+		throw new Error("globalThis.TextDecoder is not available, polyfill required");
+	}
+
+	const encoder = new TextEncoder("utf-8");
+	const decoder = new TextDecoder("utf-8");
+
+	globalThis.Go = class {
+		constructor() {
+			this.argv = ["js"];
+			this.env = {};
+			this.exit = (code) => {
+				if (code !== 0) {
+					console.warn("exit code:", code);
+				}
+			};
+			this._exitPromise = new Promise((resolve) => {
+				this._resolveExitPromise = resolve;
+			});
+			this._pendingEvent = null;
+			this._scheduledTimeouts = new Map();
+			this._nextCallbackTimeoutID = 1;
+
+			const setInt64 = (addr, v) => {
+				this.mem.setUint32(addr + 0, v, true);
+				this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
+			}
+
+			const setInt32 = (addr, v) => {
+				this.mem.setUint32(addr + 0, v, true);
+			}
+
+			const getInt64 = (addr) => {
+				const low = this.mem.getUint32(addr + 0, true);
+				const high = this.mem.getInt32(addr + 4, true);
+				return low + high * 4294967296;
+			}
+
+			const loadValue = (addr) => {
+				const f = this.mem.getFloat64(addr, true);
+				if (f === 0) {
+					return undefined;
+				}
+				if (!isNaN(f)) {
+					return f;
+				}
+
+				const id = this.mem.getUint32(addr, true);
+				return this._values[id];
+			}
+
+			const storeValue = (addr, v) => {
+				const nanHead = 0x7FF80000;
+
+				if (typeof v === "number" && v !== 0) {
+					if (isNaN(v)) {
+						this.mem.setUint32(addr + 4, nanHead, true);
+						this.mem.setUint32(addr, 0, true);
+						return;
+					}
+					this.mem.setFloat64(addr, v, true);
+					return;
+				}
+
+				if (v === undefined) {
+					this.mem.setFloat64(addr, 0, true);
+					return;
+				}
+
+				let id = this._ids.get(v);
+				if (id === undefined) {
+					id = this._idPool.pop();
+					if (id === undefined) {
+						id = this._values.length;
+					}
+					this._values[id] = v;
+					this._goRefCounts[id] = 0;
+					this._ids.set(v, id);
+				}
+				this._goRefCounts[id]++;
+				let typeFlag = 0;
+				switch (typeof v) {
+					case "object":
+						if (v !== null) {
+							typeFlag = 1;
+						}
+						break;
+					case "string":
+						typeFlag = 2;
+						break;
+					case "symbol":
+						typeFlag = 3;
+						break;
+					case "function":
+						typeFlag = 4;
+						break;
+				}
+				this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
+				this.mem.setUint32(addr, id, true);
+			}
+
+			const loadSlice = (addr) => {
+				const array = getInt64(addr + 0);
+				const len = getInt64(addr + 8);
+				return new Uint8Array(this._inst.exports.mem.buffer, array, len);
+			}
+
+			const loadSliceOfValues = (addr) => {
+				const array = getInt64(addr + 0);
+				const len = getInt64(addr + 8);
+				const a = new Array(len);
+				for (let i = 0; i < len; i++) {
+					a[i] = loadValue(array + i * 8);
+				}
+				return a;
+			}
+
+			const loadString = (addr) => {
+				const saddr = getInt64(addr + 0);
+				const len = getInt64(addr + 8);
+				return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
+			}
+
+			const timeOrigin = Date.now() - performance.now();
+			this.importObject = {
+				_gotest: {
+					add: (a, b) => a + b,
+				},
+				gojs: {
+					// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
+					// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
+					// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
+					// This changes the SP, thus we have to update the SP used by the imported function.
+
+					// func wasmExit(code int32)
+					"runtime.wasmExit": (sp) => {
+						sp >>>= 0;
+						const code = this.mem.getInt32(sp + 8, true);
+						this.exited = true;
+						delete this._inst;
+						delete this._values;
+						delete this._goRefCounts;
+						delete this._ids;
+						delete this._idPool;
+						this.exit(code);
+					},
+
+					// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
+					"runtime.wasmWrite": (sp) => {
+						sp >>>= 0;
+						const fd = getInt64(sp + 8);
+						const p = getInt64(sp + 16);
+						const n = this.mem.getInt32(sp + 24, true);
+						fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
+					},
+
+					// func resetMemoryDataView()
+					"runtime.resetMemoryDataView": (sp) => {
+						sp >>>= 0;
+						this.mem = new DataView(this._inst.exports.mem.buffer);
+					},
+
+					// func nanotime1() int64
+					"runtime.nanotime1": (sp) => {
+						sp >>>= 0;
+						setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
+					},
+
+					// func walltime() (sec int64, nsec int32)
+					"runtime.walltime": (sp) => {
+						sp >>>= 0;
+						const msec = (new Date).getTime();
+						setInt64(sp + 8, msec / 1000);
+						this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
+					},
+
+					// func scheduleTimeoutEvent(delay int64) int32
+					"runtime.scheduleTimeoutEvent": (sp) => {
+						sp >>>= 0;
+						const id = this._nextCallbackTimeoutID;
+						this._nextCallbackTimeoutID++;
+						this._scheduledTimeouts.set(id, setTimeout(
+							() => {
+								this._resume();
+								while (this._scheduledTimeouts.has(id)) {
+									// for some reason Go failed to register the timeout event, log and try again
+									// (temporary workaround for https://github.com/golang/go/issues/28975)
+									console.warn("scheduleTimeoutEvent: missed timeout event");
+									this._resume();
+								}
+							},
+							getInt64(sp + 8),
+						));
+						this.mem.setInt32(sp + 16, id, true);
+					},
+
+					// func clearTimeoutEvent(id int32)
+					"runtime.clearTimeoutEvent": (sp) => {
+						sp >>>= 0;
+						const id = this.mem.getInt32(sp + 8, true);
+						clearTimeout(this._scheduledTimeouts.get(id));
+						this._scheduledTimeouts.delete(id);
+					},
+
+					// func getRandomData(r []byte)
+					"runtime.getRandomData": (sp) => {
+						sp >>>= 0;
+						crypto.getRandomValues(loadSlice(sp + 8));
+					},
+
+					// func finalizeRef(v ref)
+					"syscall/js.finalizeRef": (sp) => {
+						sp >>>= 0;
+						const id = this.mem.getUint32(sp + 8, true);
+						this._goRefCounts[id]--;
+						if (this._goRefCounts[id] === 0) {
+							const v = this._values[id];
+							this._values[id] = null;
+							this._ids.delete(v);
+							this._idPool.push(id);
+						}
+					},
+
+					// func stringVal(value string) ref
+					"syscall/js.stringVal": (sp) => {
+						sp >>>= 0;
+						storeValue(sp + 24, loadString(sp + 8));
+					},
+
+					// func valueGet(v ref, p string) ref
+					"syscall/js.valueGet": (sp) => {
+						sp >>>= 0;
+						const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
+						sp = this._inst.exports.getsp() >>> 0; // see comment above
+						storeValue(sp + 32, result);
+					},
+
+					// func valueSet(v ref, p string, x ref)
+					"syscall/js.valueSet": (sp) => {
+						sp >>>= 0;
+						Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
+					},
+
+					// func valueDelete(v ref, p string)
+					"syscall/js.valueDelete": (sp) => {
+						sp >>>= 0;
+						Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
+					},
+
+					// func valueIndex(v ref, i int) ref
+					"syscall/js.valueIndex": (sp) => {
+						sp >>>= 0;
+						storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
+					},
+
+					// valueSetIndex(v ref, i int, x ref)
+					"syscall/js.valueSetIndex": (sp) => {
+						sp >>>= 0;
+						Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
+					},
+
+					// func valueCall(v ref, m string, args []ref) (ref, bool)
+					"syscall/js.valueCall": (sp) => {
+						sp >>>= 0;
+						try {
+							const v = loadValue(sp + 8);
+							const m = Reflect.get(v, loadString(sp + 16));
+							const args = loadSliceOfValues(sp + 32);
+							const result = Reflect.apply(m, v, args);
+							sp = this._inst.exports.getsp() >>> 0; // see comment above
+							storeValue(sp + 56, result);
+							this.mem.setUint8(sp + 64, 1);
+						} catch (err) {
+							sp = this._inst.exports.getsp() >>> 0; // see comment above
+							storeValue(sp + 56, err);
+							this.mem.setUint8(sp + 64, 0);
+						}
+					},
+
+					// func valueInvoke(v ref, args []ref) (ref, bool)
+					"syscall/js.valueInvoke": (sp) => {
+						sp >>>= 0;
+						try {
+							const v = loadValue(sp + 8);
+							const args = loadSliceOfValues(sp + 16);
+							const result = Reflect.apply(v, undefined, args);
+							sp = this._inst.exports.getsp() >>> 0; // see comment above
+							storeValue(sp + 40, result);
+							this.mem.setUint8(sp + 48, 1);
+						} catch (err) {
+							sp = this._inst.exports.getsp() >>> 0; // see comment above
+							storeValue(sp + 40, err);
+							this.mem.setUint8(sp + 48, 0);
+						}
+					},
+
+					// func valueNew(v ref, args []ref) (ref, bool)
+					"syscall/js.valueNew": (sp) => {
+						sp >>>= 0;
+						try {
+							const v = loadValue(sp + 8);
+							const args = loadSliceOfValues(sp + 16);
+							const result = Reflect.construct(v, args);
+							sp = this._inst.exports.getsp() >>> 0; // see comment above
+							storeValue(sp + 40, result);
+							this.mem.setUint8(sp + 48, 1);
+						} catch (err) {
+							sp = this._inst.exports.getsp() >>> 0; // see comment above
+							storeValue(sp + 40, err);
+							this.mem.setUint8(sp + 48, 0);
+						}
+					},
+
+					// func valueLength(v ref) int
+					"syscall/js.valueLength": (sp) => {
+						sp >>>= 0;
+						setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
+					},
+
+					// valuePrepareString(v ref) (ref, int)
+					"syscall/js.valuePrepareString": (sp) => {
+						sp >>>= 0;
+						const str = encoder.encode(String(loadValue(sp + 8)));
+						storeValue(sp + 16, str);
+						setInt64(sp + 24, str.length);
+					},
+
+					// valueLoadString(v ref, b []byte)
+					"syscall/js.valueLoadString": (sp) => {
+						sp >>>= 0;
+						const str = loadValue(sp + 8);
+						loadSlice(sp + 16).set(str);
+					},
+
+					// func valueInstanceOf(v ref, t ref) bool
+					"syscall/js.valueInstanceOf": (sp) => {
+						sp >>>= 0;
+						this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
+					},
+
+					// func copyBytesToGo(dst []byte, src ref) (int, bool)
+					"syscall/js.copyBytesToGo": (sp) => {
+						sp >>>= 0;
+						const dst = loadSlice(sp + 8);
+						const src = loadValue(sp + 32);
+						if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
+							this.mem.setUint8(sp + 48, 0);
+							return;
+						}
+						const toCopy = src.subarray(0, dst.length);
+						dst.set(toCopy);
+						setInt64(sp + 40, toCopy.length);
+						this.mem.setUint8(sp + 48, 1);
+					},
+
+					// func copyBytesToJS(dst ref, src []byte) (int, bool)
+					"syscall/js.copyBytesToJS": (sp) => {
+						sp >>>= 0;
+						const dst = loadValue(sp + 8);
+						const src = loadSlice(sp + 16);
+						if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
+							this.mem.setUint8(sp + 48, 0);
+							return;
+						}
+						const toCopy = src.subarray(0, dst.length);
+						dst.set(toCopy);
+						setInt64(sp + 40, toCopy.length);
+						this.mem.setUint8(sp + 48, 1);
+					},
+
+					"debug": (value) => {
+						console.log(value);
+					},
+				}
+			};
+		}
+
+		async run(instance) {
+			if (!(instance instanceof WebAssembly.Instance)) {
+				throw new Error("Go.run: WebAssembly.Instance expected");
+			}
+			this._inst = instance;
+			this.mem = new DataView(this._inst.exports.mem.buffer);
+			this._values = [ // JS values that Go currently has references to, indexed by reference id
+				NaN,
+				0,
+				null,
+				true,
+				false,
+				globalThis,
+				this,
+			];
+			this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
+			this._ids = new Map([ // mapping from JS values to reference ids
+				[0, 1],
+				[null, 2],
+				[true, 3],
+				[false, 4],
+				[globalThis, 5],
+				[this, 6],
+			]);
+			this._idPool = [];   // unused ids that have been garbage collected
+			this.exited = false; // whether the Go program has exited
+
+			// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
+			let offset = 4096;
+
+			const strPtr = (str) => {
+				const ptr = offset;
+				const bytes = encoder.encode(str + "\0");
+				new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
+				offset += bytes.length;
+				if (offset % 8 !== 0) {
+					offset += 8 - (offset % 8);
+				}
+				return ptr;
+			};
+
+			const argc = this.argv.length;
+
+			const argvPtrs = [];
+			this.argv.forEach((arg) => {
+				argvPtrs.push(strPtr(arg));
+			});
+			argvPtrs.push(0);
+
+			const keys = Object.keys(this.env).sort();
+			keys.forEach((key) => {
+				argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
+			});
+			argvPtrs.push(0);
+
+			const argv = offset;
+			argvPtrs.forEach((ptr) => {
+				this.mem.setUint32(offset, ptr, true);
+				this.mem.setUint32(offset + 4, 0, true);
+				offset += 8;
+			});
+
+			// The linker guarantees global data starts from at least wasmMinDataAddr.
+			// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
+			const wasmMinDataAddr = 4096 + 8192;
+			if (offset >= wasmMinDataAddr) {
+				throw new Error("total length of command line and environment variables exceeds limit");
+			}
+
+			this._inst.exports.run(argc, argv);
+			if (this.exited) {
+				this._resolveExitPromise();
+			}
+			await this._exitPromise;
+		}
+
+		_resume() {
+			if (this.exited) {
+				throw new Error("Go program has already exited");
+			}
+			this._inst.exports.resume();
+			if (this.exited) {
+				this._resolveExitPromise();
+			}
+		}
+
+		_makeFuncWrapper(id) {
+			const go = this;
+			return function () {
+				const event = { id: id, this: this, args: arguments };
+				go._pendingEvent = event;
+				go._resume();
+				return event.result;
+			};
+		}
+	}
+})();

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
public/worker-legacy.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
public/worker.js


+ 9 - 0
src/api/company/companyUser.js

@@ -385,3 +385,12 @@ export function unBind(userId) {
     data: { userId: userId }
   })
 }
+
+// 即时通账号校验(OpenIM 登录前置接口)
+export function accountCheck(userId) {
+  return request({
+    url: '/company/user/accountCheck',
+    method: 'post',
+    data: { userId: userId }
+  })
+}

+ 16 - 0
src/api/company/friend.js

@@ -0,0 +1,16 @@
+import request from '@/utils/request'
+export function getUserList(phone) {
+  return request({
+    url: '/company/friend/getUserList',
+    method: 'get',
+    params: { phone }
+  })
+}
+export function addFriend(userId) {
+  return request({
+    url: '/company/friend/addFriend',
+    method: 'post',
+    data: userId,
+  })
+}
+

+ 68 - 0
src/api/coupon/coupon.js

@@ -0,0 +1,68 @@
+import request from '@/utils/request'
+
+// 查询优惠券列表
+export function listCoupon(query) {
+  return request({
+    url: '/company/coupon/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询优惠券详细
+export function getCoupon(couponId) {
+  return request({
+    url: '/company/coupon/' + couponId,
+    method: 'get'
+  })
+}
+
+export function allList() {
+  return request({
+    url: '/company/coupon/allList',
+    method: 'get'
+  })
+}
+// 新增优惠券
+export function addCoupon(data) {
+  return request({
+    url: '/company/coupon',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改优惠券
+export function updateCoupon(data) {
+  return request({
+    url: '/company/coupon',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除优惠券
+export function delCoupon(couponIds) {
+  return request({
+    url: '/company/coupon/' + couponIds,
+    method: 'delete'
+  })
+}
+
+// 导出优惠券
+export function exportCoupon(query) {
+  return request({
+    url: '/company/coupon/export',
+    method: 'get',
+    params: query
+  })
+}
+
+export function sendCoupon(data) {
+  return request({
+    url: '/company/coupon/sendCoupon',
+    method: 'post',
+    data: data
+  })
+}
+

+ 18 - 0
src/api/group.js

@@ -0,0 +1,18 @@
+import request from '@/utils/request'
+
+export function createGroup(data) {
+  return request({
+    url: '/company/openImGroup/createGroup',
+    method: 'post',
+    data: data
+  })
+}
+
+export function addFriend(userId) {
+  return request({
+    url: '/company/openImGroup/addFriend',
+    method: 'post',
+    params: { userId }
+  })
+}
+

+ 1 - 0
src/assets/audio/beCalled.mp3

@@ -0,0 +1 @@
+placeholder

+ 3055 - 0
src/assets/css/animate.css

@@ -0,0 +1,3055 @@
+@charset "UTF-8";
+
+/*!
+ * animate.css -http://daneden.me/animate
+ * Version - 3.5.0
+ * Licensed under the MIT license - http://opensource.org/licenses/MIT
+ *
+ * Copyright (c) 2016 Daniel Eden
+ */
+
+.animated {
+    -webkit-animation-duration: 0.6s;
+    animation-duration: 0.6s;
+    -webkit-animation-fill-mode: both;
+    animation-fill-mode: both
+}
+
+.animated.infinite {
+    -webkit-animation-iteration-count: infinite;
+    animation-iteration-count: infinite
+}
+
+.animated.hinge {
+    -webkit-animation-duration: 2s;
+    animation-duration: 2s
+}
+
+.animated.bounceIn, .animated.bounceOut, .animated.flipOutX, .animated.flipOutY {
+    -webkit-animation-duration: .75s;
+    animation-duration: .75s
+}
+
+@-webkit-keyframes bounce {
+    0%, 20%, 53%, 80%, to {
+        -webkit-animation-timing-function: cubic-bezier(.215, .61, .355, 1);
+        animation-timing-function: cubic-bezier(.215, .61, .355, 1);
+        -webkit-transform: translateZ(0);
+        transform: translateZ(0)
+    }
+    40%, 43% {
+        -webkit-transform: translate3d(0, -30px, 0);
+        transform: translate3d(0, -30px, 0)
+    }
+    40%, 43%, 70% {
+        -webkit-animation-timing-function: cubic-bezier(.755, .05, .855, .06);
+        animation-timing-function: cubic-bezier(.755, .05, .855, .06)
+    }
+    70% {
+        -webkit-transform: translate3d(0, -15px, 0);
+        transform: translate3d(0, -15px, 0)
+    }
+    90% {
+        -webkit-transform: translate3d(0, -4px, 0);
+        transform: translate3d(0, -4px, 0)
+    }
+}
+
+@keyframes bounce {
+    0%, 20%, 53%, 80%, to {
+        -webkit-animation-timing-function: cubic-bezier(.215, .61, .355, 1);
+        animation-timing-function: cubic-bezier(.215, .61, .355, 1);
+        -webkit-transform: translateZ(0);
+        transform: translateZ(0)
+    }
+    40%, 43% {
+        -webkit-transform: translate3d(0, -30px, 0);
+        transform: translate3d(0, -30px, 0)
+    }
+    40%, 43%, 70% {
+        -webkit-animation-timing-function: cubic-bezier(.755, .05, .855, .06);
+        animation-timing-function: cubic-bezier(.755, .05, .855, .06)
+    }
+    70% {
+        -webkit-transform: translate3d(0, -15px, 0);
+        transform: translate3d(0, -15px, 0)
+    }
+    90% {
+        -webkit-transform: translate3d(0, -4px, 0);
+        transform: translate3d(0, -4px, 0)
+    }
+}
+
+.bounce {
+    -webkit-animation-name: bounce;
+    animation-name: bounce;
+    -webkit-transform-origin: center bottom;
+    transform-origin: center bottom
+}
+
+@-webkit-keyframes flash {
+    0%, 50%, to {
+        opacity: 1
+    }
+    25%, 75% {
+        opacity: 0
+    }
+}
+
+@keyframes flash {
+    0%, 50%, to {
+        opacity: 1
+    }
+    25%, 75% {
+        opacity: 0
+    }
+}
+
+.flash {
+    -webkit-animation-name: flash;
+    animation-name: flash
+}
+
+@-webkit-keyframes pulse {
+    0% {
+        -webkit-transform: scaleX(1);
+        transform: scaleX(1)
+    }
+    50% {
+        -webkit-transform: scale3d(1.05, 1.05, 1.05);
+        transform: scale3d(1.05, 1.05, 1.05)
+    }
+    to {
+        -webkit-transform: scaleX(1);
+        transform: scaleX(1)
+    }
+}
+
+@keyframes pulse {
+    0% {
+        -webkit-transform: scaleX(1);
+        transform: scaleX(1)
+    }
+    50% {
+        -webkit-transform: scale3d(1.05, 1.05, 1.05);
+        transform: scale3d(1.05, 1.05, 1.05)
+    }
+    to {
+        -webkit-transform: scaleX(1);
+        transform: scaleX(1)
+    }
+}
+
+.pulse {
+    -webkit-animation-name: pulse;
+    animation-name: pulse
+}
+
+@-webkit-keyframes rubberBand {
+    0% {
+        -webkit-transform: scaleX(1);
+        transform: scaleX(1)
+    }
+    30% {
+        -webkit-transform: scale3d(1.25, .75, 1);
+        transform: scale3d(1.25, .75, 1)
+    }
+    40% {
+        -webkit-transform: scale3d(.75, 1.25, 1);
+        transform: scale3d(.75, 1.25, 1)
+    }
+    50% {
+        -webkit-transform: scale3d(1.15, .85, 1);
+        transform: scale3d(1.15, .85, 1)
+    }
+    65% {
+        -webkit-transform: scale3d(.95, 1.05, 1);
+        transform: scale3d(.95, 1.05, 1)
+    }
+    75% {
+        -webkit-transform: scale3d(1.05, .95, 1);
+        transform: scale3d(1.05, .95, 1)
+    }
+    to {
+        -webkit-transform: scaleX(1);
+        transform: scaleX(1)
+    }
+}
+
+@keyframes rubberBand {
+    0% {
+        -webkit-transform: scaleX(1);
+        transform: scaleX(1)
+    }
+    30% {
+        -webkit-transform: scale3d(1.25, .75, 1);
+        transform: scale3d(1.25, .75, 1)
+    }
+    40% {
+        -webkit-transform: scale3d(.75, 1.25, 1);
+        transform: scale3d(.75, 1.25, 1)
+    }
+    50% {
+        -webkit-transform: scale3d(1.15, .85, 1);
+        transform: scale3d(1.15, .85, 1)
+    }
+    65% {
+        -webkit-transform: scale3d(.95, 1.05, 1);
+        transform: scale3d(.95, 1.05, 1)
+    }
+    75% {
+        -webkit-transform: scale3d(1.05, .95, 1);
+        transform: scale3d(1.05, .95, 1)
+    }
+    to {
+        -webkit-transform: scaleX(1);
+        transform: scaleX(1)
+    }
+}
+
+.rubberBand {
+    -webkit-animation-name: rubberBand;
+    animation-name: rubberBand
+}
+
+@-webkit-keyframes shake {
+    0%, to {
+        -webkit-transform: translateZ(0);
+        transform: translateZ(0)
+    }
+    10%, 30%, 50%, 70%, 90% {
+        -webkit-transform: translate3d(-10px, 0, 0);
+        transform: translate3d(-10px, 0, 0)
+    }
+    20%, 40%, 60%, 80% {
+        -webkit-transform: translate3d(10px, 0, 0);
+        transform: translate3d(10px, 0, 0)
+    }
+}
+
+@keyframes shake {
+    0%, to {
+        -webkit-transform: translateZ(0);
+        transform: translateZ(0)
+    }
+    10%, 30%, 50%, 70%, 90% {
+        -webkit-transform: translate3d(-10px, 0, 0);
+        transform: translate3d(-10px, 0, 0)
+    }
+    20%, 40%, 60%, 80% {
+        -webkit-transform: translate3d(10px, 0, 0);
+        transform: translate3d(10px, 0, 0)
+    }
+}
+
+.shake {
+    -webkit-animation-name: shake;
+    animation-name: shake
+}
+
+@-webkit-keyframes headShake {
+    0% {
+        -webkit-transform: translateX(0);
+        transform: translateX(0)
+    }
+    6.5% {
+        -webkit-transform: translateX(-6px) rotateY(-9deg);
+        transform: translateX(-6px) rotateY(-9deg)
+    }
+    18.5% {
+        -webkit-transform: translateX(5px) rotateY(7deg);
+        transform: translateX(5px) rotateY(7deg)
+    }
+    31.5% {
+        -webkit-transform: translateX(-3px) rotateY(-5deg);
+        transform: translateX(-3px) rotateY(-5deg)
+    }
+    43.5% {
+        -webkit-transform: translateX(2px) rotateY(3deg);
+        transform: translateX(2px) rotateY(3deg)
+    }
+    50% {
+        -webkit-transform: translateX(0);
+        transform: translateX(0)
+    }
+}
+
+@keyframes headShake {
+    0% {
+        -webkit-transform: translateX(0);
+        transform: translateX(0)
+    }
+    6.5% {
+        -webkit-transform: translateX(-6px) rotateY(-9deg);
+        transform: translateX(-6px) rotateY(-9deg)
+    }
+    18.5% {
+        -webkit-transform: translateX(5px) rotateY(7deg);
+        transform: translateX(5px) rotateY(7deg)
+    }
+    31.5% {
+        -webkit-transform: translateX(-3px) rotateY(-5deg);
+        transform: translateX(-3px) rotateY(-5deg)
+    }
+    43.5% {
+        -webkit-transform: translateX(2px) rotateY(3deg);
+        transform: translateX(2px) rotateY(3deg)
+    }
+    50% {
+        -webkit-transform: translateX(0);
+        transform: translateX(0)
+    }
+}
+
+.headShake {
+    -webkit-animation-timing-function: ease-in-out;
+    animation-timing-function: ease-in-out;
+    -webkit-animation-name: headShake;
+    animation-name: headShake
+}
+
+@-webkit-keyframes swing {
+    20% {
+        -webkit-transform: rotate(15deg);
+        transform: rotate(15deg)
+    }
+    40% {
+        -webkit-transform: rotate(-10deg);
+        transform: rotate(-10deg)
+    }
+    60% {
+        -webkit-transform: rotate(5deg);
+        transform: rotate(5deg)
+    }
+    80% {
+        -webkit-transform: rotate(-5deg);
+        transform: rotate(-5deg)
+    }
+    to {
+        -webkit-transform: rotate(0deg);
+        transform: rotate(0deg)
+    }
+}
+
+@keyframes swing {
+    20% {
+        -webkit-transform: rotate(15deg);
+        transform: rotate(15deg)
+    }
+    40% {
+        -webkit-transform: rotate(-10deg);
+        transform: rotate(-10deg)
+    }
+    60% {
+        -webkit-transform: rotate(5deg);
+        transform: rotate(5deg)
+    }
+    80% {
+        -webkit-transform: rotate(-5deg);
+        transform: rotate(-5deg)
+    }
+    to {
+        -webkit-transform: rotate(0deg);
+        transform: rotate(0deg)
+    }
+}
+
+.swing {
+    -webkit-transform-origin: top center;
+    transform-origin: top center;
+    -webkit-animation-name: swing;
+    animation-name: swing
+}
+
+@-webkit-keyframes tada {
+    0% {
+        -webkit-transform: scaleX(1);
+        transform: scaleX(1)
+    }
+    10%, 20% {
+        -webkit-transform: scale3d(.9, .9, .9) rotate(-3deg);
+        transform: scale3d(.9, .9, .9) rotate(-3deg)
+    }
+    30%, 50%, 70%, 90% {
+        -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate(3deg);
+        transform: scale3d(1.1, 1.1, 1.1) rotate(3deg)
+    }
+    40%, 60%, 80% {
+        -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate(-3deg);
+        transform: scale3d(1.1, 1.1, 1.1) rotate(-3deg)
+    }
+    to {
+        -webkit-transform: scaleX(1);
+        transform: scaleX(1)
+    }
+}
+
+@keyframes tada {
+    0% {
+        -webkit-transform: scaleX(1);
+        transform: scaleX(1)
+    }
+    10%, 20% {
+        -webkit-transform: scale3d(.9, .9, .9) rotate(-3deg);
+        transform: scale3d(.9, .9, .9) rotate(-3deg)
+    }
+    30%, 50%, 70%, 90% {
+        -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate(3deg);
+        transform: scale3d(1.1, 1.1, 1.1) rotate(3deg)
+    }
+    40%, 60%, 80% {
+        -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate(-3deg);
+        transform: scale3d(1.1, 1.1, 1.1) rotate(-3deg)
+    }
+    to {
+        -webkit-transform: scaleX(1);
+        transform: scaleX(1)
+    }
+}
+
+.tada {
+    -webkit-animation-name: tada;
+    animation-name: tada
+}
+
+@-webkit-keyframes wobble {
+    0% {
+        -webkit-transform: none;
+        transform: none
+    }
+    15% {
+        -webkit-transform: translate3d(-25%, 0, 0) rotate(-5deg);
+        transform: translate3d(-25%, 0, 0) rotate(-5deg)
+    }
+    30% {
+        -webkit-transform: translate3d(20%, 0, 0) rotate(3deg);
+        transform: translate3d(20%, 0, 0) rotate(3deg)
+    }
+    45% {
+        -webkit-transform: translate3d(-15%, 0, 0) rotate(-3deg);
+        transform: translate3d(-15%, 0, 0) rotate(-3deg)
+    }
+    60% {
+        -webkit-transform: translate3d(10%, 0, 0) rotate(2deg);
+        transform: translate3d(10%, 0, 0) rotate(2deg)
+    }
+    75% {
+        -webkit-transform: translate3d(-5%, 0, 0) rotate(-1deg);
+        transform: translate3d(-5%, 0, 0) rotate(-1deg)
+    }
+    to {
+        -webkit-transform: none;
+        transform: none
+    }
+}
+
+@keyframes wobble {
+    0% {
+        -webkit-transform: none;
+        transform: none
+    }
+    15% {
+        -webkit-transform: translate3d(-25%, 0, 0) rotate(-5deg);
+        transform: translate3d(-25%, 0, 0) rotate(-5deg)
+    }
+    30% {
+        -webkit-transform: translate3d(20%, 0, 0) rotate(3deg);
+        transform: translate3d(20%, 0, 0) rotate(3deg)
+    }
+    45% {
+        -webkit-transform: translate3d(-15%, 0, 0) rotate(-3deg);
+        transform: translate3d(-15%, 0, 0) rotate(-3deg)
+    }
+    60% {
+        -webkit-transform: translate3d(10%, 0, 0) rotate(2deg);
+        transform: translate3d(10%, 0, 0) rotate(2deg)
+    }
+    75% {
+        -webkit-transform: translate3d(-5%, 0, 0) rotate(-1deg);
+        transform: translate3d(-5%, 0, 0) rotate(-1deg)
+    }
+    to {
+        -webkit-transform: none;
+        transform: none
+    }
+}
+
+.wobble {
+    -webkit-animation-name: wobble;
+    animation-name: wobble
+}
+
+@-webkit-keyframes jello {
+    0%, 11.1%, to {
+        -webkit-transform: none;
+        transform: none
+    }
+    22.2% {
+        -webkit-transform: skewX(-12.5deg) skewY(-12.5deg);
+        transform: skewX(-12.5deg) skewY(-12.5deg)
+    }
+    33.3% {
+        -webkit-transform: skewX(6.25deg) skewY(6.25deg);
+        transform: skewX(6.25deg) skewY(6.25deg)
+    }
+    44.4% {
+        -webkit-transform: skewX(-3.125deg) skewY(-3.125deg);
+        transform: skewX(-3.125deg) skewY(-3.125deg)
+    }
+    55.5% {
+        -webkit-transform: skewX(1.5625deg) skewY(1.5625deg);
+        transform: skewX(1.5625deg) skewY(1.5625deg)
+    }
+    66.6% {
+        -webkit-transform: skewX(-.78125deg) skewY(-.78125deg);
+        transform: skewX(-.78125deg) skewY(-.78125deg)
+    }
+    77.7% {
+        -webkit-transform: skewX(.390625deg) skewY(.390625deg);
+        transform: skewX(.390625deg) skewY(.390625deg)
+    }
+    88.8% {
+        -webkit-transform: skewX(-.1953125deg) skewY(-.1953125deg);
+        transform: skewX(-.1953125deg) skewY(-.1953125deg)
+    }
+}
+
+@keyframes jello {
+    0%, 11.1%, to {
+        -webkit-transform: none;
+        transform: none
+    }
+    22.2% {
+        -webkit-transform: skewX(-12.5deg) skewY(-12.5deg);
+        transform: skewX(-12.5deg) skewY(-12.5deg)
+    }
+    33.3% {
+        -webkit-transform: skewX(6.25deg) skewY(6.25deg);
+        transform: skewX(6.25deg) skewY(6.25deg)
+    }
+    44.4% {
+        -webkit-transform: skewX(-3.125deg) skewY(-3.125deg);
+        transform: skewX(-3.125deg) skewY(-3.125deg)
+    }
+    55.5% {
+        -webkit-transform: skewX(1.5625deg) skewY(1.5625deg);
+        transform: skewX(1.5625deg) skewY(1.5625deg)
+    }
+    66.6% {
+        -webkit-transform: skewX(-.78125deg) skewY(-.78125deg);
+        transform: skewX(-.78125deg) skewY(-.78125deg)
+    }
+    77.7% {
+        -webkit-transform: skewX(.390625deg) skewY(.390625deg);
+        transform: skewX(.390625deg) skewY(.390625deg)
+    }
+    88.8% {
+        -webkit-transform: skewX(-.1953125deg) skewY(-.1953125deg);
+        transform: skewX(-.1953125deg) skewY(-.1953125deg)
+    }
+}
+
+.jello {
+    -webkit-animation-name: jello;
+    animation-name: jello;
+    -webkit-transform-origin: center;
+    transform-origin: center
+}
+
+@-webkit-keyframes bounceIn {
+    0%, 20%, 40%, 60%, 80%, to {
+        -webkit-animation-timing-function: cubic-bezier(.215, .61, .355, 1);
+        animation-timing-function: cubic-bezier(.215, .61, .355, 1)
+    }
+    0% {
+        opacity: 0;
+        -webkit-transform: scale3d(.3, .3, .3);
+        transform: scale3d(.3, .3, .3)
+    }
+    20% {
+        -webkit-transform: scale3d(1.1, 1.1, 1.1);
+        transform: scale3d(1.1, 1.1, 1.1)
+    }
+    40% {
+        -webkit-transform: scale3d(.9, .9, .9);
+        transform: scale3d(.9, .9, .9)
+    }
+    60% {
+        opacity: 1;
+        -webkit-transform: scale3d(1.03, 1.03, 1.03);
+        transform: scale3d(1.03, 1.03, 1.03)
+    }
+    80% {
+        -webkit-transform: scale3d(.97, .97, .97);
+        transform: scale3d(.97, .97, .97)
+    }
+    to {
+        opacity: 1;
+        -webkit-transform: scaleX(1);
+        transform: scaleX(1)
+    }
+}
+
+@keyframes bounceIn {
+    0%, 20%, 40%, 60%, 80%, to {
+        -webkit-animation-timing-function: cubic-bezier(.215, .61, .355, 1);
+        animation-timing-function: cubic-bezier(.215, .61, .355, 1)
+    }
+    0% {
+        opacity: 0;
+        -webkit-transform: scale3d(.3, .3, .3);
+        transform: scale3d(.3, .3, .3)
+    }
+    20% {
+        -webkit-transform: scale3d(1.1, 1.1, 1.1);
+        transform: scale3d(1.1, 1.1, 1.1)
+    }
+    40% {
+        -webkit-transform: scale3d(.9, .9, .9);
+        transform: scale3d(.9, .9, .9)
+    }
+    60% {
+        opacity: 1;
+        -webkit-transform: scale3d(1.03, 1.03, 1.03);
+        transform: scale3d(1.03, 1.03, 1.03)
+    }
+    80% {
+        -webkit-transform: scale3d(.97, .97, .97);
+        transform: scale3d(.97, .97, .97)
+    }
+    to {
+        opacity: 1;
+        -webkit-transform: scaleX(1);
+        transform: scaleX(1)
+    }
+}
+
+.bounceIn {
+    -webkit-animation-name: bounceIn;
+    animation-name: bounceIn
+}
+
+@-webkit-keyframes bounceInDown {
+    0%, 60%, 75%, 90%, to {
+        -webkit-animation-timing-function: cubic-bezier(.215, .61, .355, 1);
+        animation-timing-function: cubic-bezier(.215, .61, .355, 1)
+    }
+    0% {
+        opacity: 0;
+        -webkit-transform: translate3d(0, -3000px, 0);
+        transform: translate3d(0, -3000px, 0)
+    }
+    60% {
+        opacity: 1;
+        -webkit-transform: translate3d(0, 25px, 0);
+        transform: translate3d(0, 25px, 0)
+    }
+    75% {
+        -webkit-transform: translate3d(0, -10px, 0);
+        transform: translate3d(0, -10px, 0)
+    }
+    90% {
+        -webkit-transform: translate3d(0, 5px, 0);
+        transform: translate3d(0, 5px, 0)
+    }
+    to {
+        -webkit-transform: none;
+        transform: none
+    }
+}
+
+@keyframes bounceInDown {
+    0%, 60%, 75%, 90%, to {
+        -webkit-animation-timing-function: cubic-bezier(.215, .61, .355, 1);
+        animation-timing-function: cubic-bezier(.215, .61, .355, 1)
+    }
+    0% {
+        opacity: 0;
+        -webkit-transform: translate3d(0, -3000px, 0);
+        transform: translate3d(0, -3000px, 0)
+    }
+    60% {
+        opacity: 1;
+        -webkit-transform: translate3d(0, 25px, 0);
+        transform: translate3d(0, 25px, 0)
+    }
+    75% {
+        -webkit-transform: translate3d(0, -10px, 0);
+        transform: translate3d(0, -10px, 0)
+    }
+    90% {
+        -webkit-transform: translate3d(0, 5px, 0);
+        transform: translate3d(0, 5px, 0)
+    }
+    to {
+        -webkit-transform: none;
+        transform: none
+    }
+}
+
+.bounceInDown {
+    -webkit-animation-name: bounceInDown;
+    animation-name: bounceInDown
+}
+
+@-webkit-keyframes bounceInLeft {
+    0%, 60%, 75%, 90%, to {
+        -webkit-animation-timing-function: cubic-bezier(.215, .61, .355, 1);
+        animation-timing-function: cubic-bezier(.215, .61, .355, 1)
+    }
+    0% {
+        opacity: 0;
+        -webkit-transform: translate3d(-3000px, 0, 0);
+        transform: translate3d(-3000px, 0, 0)
+    }
+    60% {
+        opacity: 1;
+        -webkit-transform: translate3d(25px, 0, 0);
+        transform: translate3d(25px, 0, 0)
+    }
+    75% {
+        -webkit-transform: translate3d(-10px, 0, 0);
+        transform: translate3d(-10px, 0, 0)
+    }
+    90% {
+        -webkit-transform: translate3d(5px, 0, 0);
+        transform: translate3d(5px, 0, 0)
+    }
+    to {
+        -webkit-transform: none;
+        transform: none
+    }
+}
+
+@keyframes bounceInLeft {
+    0%, 60%, 75%, 90%, to {
+        -webkit-animation-timing-function: cubic-bezier(.215, .61, .355, 1);
+        animation-timing-function: cubic-bezier(.215, .61, .355, 1)
+    }
+    0% {
+        opacity: 0;
+        -webkit-transform: translate3d(-3000px, 0, 0);
+        transform: translate3d(-3000px, 0, 0)
+    }
+    60% {
+        opacity: 1;
+        -webkit-transform: translate3d(25px, 0, 0);
+        transform: translate3d(25px, 0, 0)
+    }
+    75% {
+        -webkit-transform: translate3d(-10px, 0, 0);
+        transform: translate3d(-10px, 0, 0)
+    }
+    90% {
+        -webkit-transform: translate3d(5px, 0, 0);
+        transform: translate3d(5px, 0, 0)
+    }
+    to {
+        -webkit-transform: none;
+        transform: none
+    }
+}
+
+.bounceInLeft {
+    -webkit-animation-name: bounceInLeft;
+    animation-name: bounceInLeft
+}
+
+@-webkit-keyframes bounceInRight {
+    0%, 60%, 75%, 90%, to {
+        -webkit-animation-timing-function: cubic-bezier(.215, .61, .355, 1);
+        animation-timing-function: cubic-bezier(.215, .61, .355, 1)
+    }
+    0% {
+        opacity: 0;
+        -webkit-transform: translate3d(3000px, 0, 0);
+        transform: translate3d(3000px, 0, 0)
+    }
+    60% {
+        opacity: 1;
+        -webkit-transform: translate3d(-25px, 0, 0);
+        transform: translate3d(-25px, 0, 0)
+    }
+    75% {
+        -webkit-transform: translate3d(10px, 0, 0);
+        transform: translate3d(10px, 0, 0)
+    }
+    90% {
+        -webkit-transform: translate3d(-5px, 0, 0);
+        transform: translate3d(-5px, 0, 0)
+    }
+    to {
+        -webkit-transform: none;
+        transform: none
+    }
+}
+
+@keyframes bounceInRight {
+    0%, 60%, 75%, 90%, to {
+        -webkit-animation-timing-function: cubic-bezier(.215, .61, .355, 1);
+        animation-timing-function: cubic-bezier(.215, .61, .355, 1)
+    }
+    0% {
+        opacity: 0;
+        -webkit-transform: translate3d(3000px, 0, 0);
+        transform: translate3d(3000px, 0, 0)
+    }
+    60% {
+        opacity: 1;
+        -webkit-transform: translate3d(-25px, 0, 0);
+        transform: translate3d(-25px, 0, 0)
+    }
+    75% {
+        -webkit-transform: translate3d(10px, 0, 0);
+        transform: translate3d(10px, 0, 0)
+    }
+    90% {
+        -webkit-transform: translate3d(-5px, 0, 0);
+        transform: translate3d(-5px, 0, 0)
+    }
+    to {
+        -webkit-transform: none;
+        transform: none
+    }
+}
+
+.bounceInRight {
+    -webkit-animation-name: bounceInRight;
+    animation-name: bounceInRight
+}
+
+@-webkit-keyframes bounceInUp {
+    0%, 60%, 75%, 90%, to {
+        -webkit-animation-timing-function: cubic-bezier(.215, .61, .355, 1);
+        animation-timing-function: cubic-bezier(.215, .61, .355, 1)
+    }
+    0% {
+        opacity: 0;
+        -webkit-transform: translate3d(0, 3000px, 0);
+        transform: translate3d(0, 3000px, 0)
+    }
+    60% {
+        opacity: 1;
+        -webkit-transform: translate3d(0, -20px, 0);
+        transform: translate3d(0, -20px, 0)
+    }
+    75% {
+        -webkit-transform: translate3d(0, 10px, 0);
+        transform: translate3d(0, 10px, 0)
+    }
+    90% {
+        -webkit-transform: translate3d(0, -5px, 0);
+        transform: translate3d(0, -5px, 0)
+    }
+    to {
+        -webkit-transform: translateZ(0);
+        transform: translateZ(0)
+    }
+}
+
+@keyframes bounceInUp {
+    0%, 60%, 75%, 90%, to {
+        -webkit-animation-timing-function: cubic-bezier(.215, .61, .355, 1);
+        animation-timing-function: cubic-bezier(.215, .61, .355, 1)
+    }
+    0% {
+        opacity: 0;
+        -webkit-transform: translate3d(0, 3000px, 0);
+        transform: translate3d(0, 3000px, 0)
+    }
+    60% {
+        opacity: 1;
+        -webkit-transform: translate3d(0, -20px, 0);
+        transform: translate3d(0, -20px, 0)
+    }
+    75% {
+        -webkit-transform: translate3d(0, 10px, 0);
+        transform: translate3d(0, 10px, 0)
+    }
+    90% {
+        -webkit-transform: translate3d(0, -5px, 0);
+        transform: translate3d(0, -5px, 0)
+    }
+    to {
+        -webkit-transform: translateZ(0);
+        transform: translateZ(0)
+    }
+}
+
+.bounceInUp {
+    -webkit-animation-name: bounceInUp;
+    animation-name: bounceInUp
+}
+
+@-webkit-keyframes bounceOut {
+    20% {
+        -webkit-transform: scale3d(.9, .9, .9);
+        transform: scale3d(.9, .9, .9)
+    }
+    50%, 55% {
+        opacity: 1;
+        -webkit-transform: scale3d(1.1, 1.1, 1.1);
+        transform: scale3d(1.1, 1.1, 1.1)
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: scale3d(.3, .3, .3);
+        transform: scale3d(.3, .3, .3)
+    }
+}
+
+@keyframes bounceOut {
+    20% {
+        -webkit-transform: scale3d(.9, .9, .9);
+        transform: scale3d(.9, .9, .9)
+    }
+    50%, 55% {
+        opacity: 1;
+        -webkit-transform: scale3d(1.1, 1.1, 1.1);
+        transform: scale3d(1.1, 1.1, 1.1)
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: scale3d(.3, .3, .3);
+        transform: scale3d(.3, .3, .3)
+    }
+}
+
+.bounceOut {
+    -webkit-animation-name: bounceOut;
+    animation-name: bounceOut
+}
+
+@-webkit-keyframes bounceOutDown {
+    20% {
+        -webkit-transform: translate3d(0, 10px, 0);
+        transform: translate3d(0, 10px, 0)
+    }
+    40%, 45% {
+        opacity: 1;
+        -webkit-transform: translate3d(0, -20px, 0);
+        transform: translate3d(0, -20px, 0)
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: translate3d(0, 2000px, 0);
+        transform: translate3d(0, 2000px, 0)
+    }
+}
+
+@keyframes bounceOutDown {
+    20% {
+        -webkit-transform: translate3d(0, 10px, 0);
+        transform: translate3d(0, 10px, 0)
+    }
+    40%, 45% {
+        opacity: 1;
+        -webkit-transform: translate3d(0, -20px, 0);
+        transform: translate3d(0, -20px, 0)
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: translate3d(0, 2000px, 0);
+        transform: translate3d(0, 2000px, 0)
+    }
+}
+
+.bounceOutDown {
+    -webkit-animation-name: bounceOutDown;
+    animation-name: bounceOutDown
+}
+
+@-webkit-keyframes bounceOutLeft {
+    20% {
+        opacity: 1;
+        -webkit-transform: translate3d(20px, 0, 0);
+        transform: translate3d(20px, 0, 0)
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: translate3d(-2000px, 0, 0);
+        transform: translate3d(-2000px, 0, 0)
+    }
+}
+
+@keyframes bounceOutLeft {
+    20% {
+        opacity: 1;
+        -webkit-transform: translate3d(20px, 0, 0);
+        transform: translate3d(20px, 0, 0)
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: translate3d(-2000px, 0, 0);
+        transform: translate3d(-2000px, 0, 0)
+    }
+}
+
+.bounceOutLeft {
+    -webkit-animation-name: bounceOutLeft;
+    animation-name: bounceOutLeft
+}
+
+@-webkit-keyframes bounceOutRight {
+    20% {
+        opacity: 1;
+        -webkit-transform: translate3d(-20px, 0, 0);
+        transform: translate3d(-20px, 0, 0)
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: translate3d(2000px, 0, 0);
+        transform: translate3d(2000px, 0, 0)
+    }
+}
+
+@keyframes bounceOutRight {
+    20% {
+        opacity: 1;
+        -webkit-transform: translate3d(-20px, 0, 0);
+        transform: translate3d(-20px, 0, 0)
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: translate3d(2000px, 0, 0);
+        transform: translate3d(2000px, 0, 0)
+    }
+}
+
+.bounceOutRight {
+    -webkit-animation-name: bounceOutRight;
+    animation-name: bounceOutRight
+}
+
+@-webkit-keyframes bounceOutUp {
+    20% {
+        -webkit-transform: translate3d(0, -10px, 0);
+        transform: translate3d(0, -10px, 0)
+    }
+    40%, 45% {
+        opacity: 1;
+        -webkit-transform: translate3d(0, 20px, 0);
+        transform: translate3d(0, 20px, 0)
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: translate3d(0, -2000px, 0);
+        transform: translate3d(0, -2000px, 0)
+    }
+}
+
+@keyframes bounceOutUp {
+    20% {
+        -webkit-transform: translate3d(0, -10px, 0);
+        transform: translate3d(0, -10px, 0)
+    }
+    40%, 45% {
+        opacity: 1;
+        -webkit-transform: translate3d(0, 20px, 0);
+        transform: translate3d(0, 20px, 0)
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: translate3d(0, -2000px, 0);
+        transform: translate3d(0, -2000px, 0)
+    }
+}
+
+.bounceOutUp {
+    -webkit-animation-name: bounceOutUp;
+    animation-name: bounceOutUp
+}
+
+@-webkit-keyframes fadeIn {
+    0% {
+        opacity: 0
+    }
+    to {
+        opacity: 1
+    }
+}
+
+@keyframes fadeIn {
+    0% {
+        opacity: 0
+    }
+    to {
+        opacity: 1
+    }
+}
+
+.fadeIn {
+    -webkit-animation-name: fadeIn;
+    animation-name: fadeIn
+}
+
+@-webkit-keyframes fadeInDown {
+    0% {
+        opacity: 0;
+        -webkit-transform: translate3d(0, -100%, 0);
+        transform: translate3d(0, -100%, 0)
+    }
+    to {
+        opacity: 1;
+        -webkit-transform: none;
+        transform: none
+    }
+}
+
+@keyframes fadeInDown {
+    0% {
+        opacity: 0;
+        -webkit-transform: translate3d(0, -100%, 0);
+        transform: translate3d(0, -100%, 0)
+    }
+    to {
+        opacity: 1;
+        -webkit-transform: none;
+        transform: none
+    }
+}
+
+.fadeInDown {
+    -webkit-animation-name: fadeInDown;
+    animation-name: fadeInDown
+}
+
+@-webkit-keyframes fadeInDownBig {
+    0% {
+        opacity: 0;
+        -webkit-transform: translate3d(0, -2000px, 0);
+        transform: translate3d(0, -2000px, 0)
+    }
+    to {
+        opacity: 1;
+        -webkit-transform: none;
+        transform: none
+    }
+}
+
+@keyframes fadeInDownBig {
+    0% {
+        opacity: 0;
+        -webkit-transform: translate3d(0, -2000px, 0);
+        transform: translate3d(0, -2000px, 0)
+    }
+    to {
+        opacity: 1;
+        -webkit-transform: none;
+        transform: none
+    }
+}
+
+.fadeInDownBig {
+    -webkit-animation-name: fadeInDownBig;
+    animation-name: fadeInDownBig
+}
+
+@-webkit-keyframes fadeInLeft {
+    0% {
+        opacity: 0;
+        -webkit-transform: translate3d(-100%, 0, 0);
+        transform: translate3d(-100%, 0, 0)
+    }
+    to {
+        opacity: 1;
+        -webkit-transform: none;
+        transform: none
+    }
+}
+
+@keyframes fadeInLeft {
+    0% {
+        opacity: 0;
+        -webkit-transform: translate3d(-100%, 0, 0);
+        transform: translate3d(-100%, 0, 0)
+    }
+    to {
+        opacity: 1;
+        -webkit-transform: none;
+        transform: none
+    }
+}
+
+.fadeInLeft {
+    -webkit-animation-name: fadeInLeft;
+    animation-name: fadeInLeft
+}
+
+@-webkit-keyframes fadeInLeftBig {
+    0% {
+        opacity: 0;
+        -webkit-transform: translate3d(-2000px, 0, 0);
+        transform: translate3d(-2000px, 0, 0)
+    }
+    to {
+        opacity: 1;
+        -webkit-transform: none;
+        transform: none
+    }
+}
+
+@keyframes fadeInLeftBig {
+    0% {
+        opacity: 0;
+        -webkit-transform: translate3d(-2000px, 0, 0);
+        transform: translate3d(-2000px, 0, 0)
+    }
+    to {
+        opacity: 1;
+        -webkit-transform: none;
+        transform: none
+    }
+}
+
+.fadeInLeftBig {
+    -webkit-animation-name: fadeInLeftBig;
+    animation-name: fadeInLeftBig
+}
+
+@-webkit-keyframes fadeInRight {
+    0% {
+        opacity: 0;
+        -webkit-transform: translate3d(100%, 0, 0);
+        transform: translate3d(100%, 0, 0)
+    }
+    to {
+        opacity: 1;
+        -webkit-transform: none;
+        transform: none
+    }
+}
+
+@keyframes fadeInRight {
+    0% {
+        opacity: 0;
+        -webkit-transform: translate3d(100%, 0, 0);
+        transform: translate3d(100%, 0, 0)
+    }
+    to {
+        opacity: 1;
+        -webkit-transform: none;
+        transform: none
+    }
+}
+
+.fadeInRight {
+    -webkit-animation-name: fadeInRight;
+    animation-name: fadeInRight
+}
+
+@-webkit-keyframes fadeInRightBig {
+    0% {
+        opacity: 0;
+        -webkit-transform: translate3d(2000px, 0, 0);
+        transform: translate3d(2000px, 0, 0)
+    }
+    to {
+        opacity: 1;
+        -webkit-transform: none;
+        transform: none
+    }
+}
+
+@keyframes fadeInRightBig {
+    0% {
+        opacity: 0;
+        -webkit-transform: translate3d(2000px, 0, 0);
+        transform: translate3d(2000px, 0, 0)
+    }
+    to {
+        opacity: 1;
+        -webkit-transform: none;
+        transform: none
+    }
+}
+
+.fadeInRightBig {
+    -webkit-animation-name: fadeInRightBig;
+    animation-name: fadeInRightBig
+}
+
+@-webkit-keyframes fadeInUp {
+    0% {
+        opacity: 0;
+        -webkit-transform: translate3d(0, 100%, 0);
+        transform: translate3d(0, 100%, 0)
+    }
+    to {
+        opacity: 1;
+        -webkit-transform: none;
+        transform: none
+    }
+}
+
+@keyframes fadeInUp {
+    0% {
+        opacity: 0;
+        -webkit-transform: translate3d(0, 100%, 0);
+        transform: translate3d(0, 100%, 0)
+    }
+    to {
+        opacity: 1;
+        -webkit-transform: none;
+        transform: none
+    }
+}
+
+.fadeInUp {
+    -webkit-animation-name: fadeInUp;
+    animation-name: fadeInUp
+}
+
+@-webkit-keyframes fadeInUpBig {
+    0% {
+        opacity: 0;
+        -webkit-transform: translate3d(0, 2000px, 0);
+        transform: translate3d(0, 2000px, 0)
+    }
+    to {
+        opacity: 1;
+        -webkit-transform: none;
+        transform: none
+    }
+}
+
+@keyframes fadeInUpBig {
+    0% {
+        opacity: 0;
+        -webkit-transform: translate3d(0, 2000px, 0);
+        transform: translate3d(0, 2000px, 0)
+    }
+    to {
+        opacity: 1;
+        -webkit-transform: none;
+        transform: none
+    }
+}
+
+.fadeInUpBig {
+    -webkit-animation-name: fadeInUpBig;
+    animation-name: fadeInUpBig
+}
+
+@-webkit-keyframes fadeOut {
+    0% {
+        opacity: 1
+    }
+    to {
+        opacity: 0
+    }
+}
+
+@keyframes fadeOut {
+    0% {
+        opacity: 1
+    }
+    to {
+        opacity: 0
+    }
+}
+
+.fadeOut {
+    -webkit-animation-name: fadeOut;
+    animation-name: fadeOut
+}
+
+@-webkit-keyframes fadeOutDown {
+    0% {
+        opacity: 1
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: translate3d(0, 100%, 0);
+        transform: translate3d(0, 100%, 0)
+    }
+}
+
+@keyframes fadeOutDown {
+    0% {
+        opacity: 1
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: translate3d(0, 100%, 0);
+        transform: translate3d(0, 100%, 0)
+    }
+}
+
+.fadeOutDown {
+    -webkit-animation-name: fadeOutDown;
+    animation-name: fadeOutDown
+}
+
+@-webkit-keyframes fadeOutDownBig {
+    0% {
+        opacity: 1
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: translate3d(0, 2000px, 0);
+        transform: translate3d(0, 2000px, 0)
+    }
+}
+
+@keyframes fadeOutDownBig {
+    0% {
+        opacity: 1
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: translate3d(0, 2000px, 0);
+        transform: translate3d(0, 2000px, 0)
+    }
+}
+
+.fadeOutDownBig {
+    -webkit-animation-name: fadeOutDownBig;
+    animation-name: fadeOutDownBig
+}
+
+@-webkit-keyframes fadeOutLeft {
+    0% {
+        opacity: 1
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: translate3d(-100%, 0, 0);
+        transform: translate3d(-100%, 0, 0)
+    }
+}
+
+@keyframes fadeOutLeft {
+    0% {
+        opacity: 1
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: translate3d(-100%, 0, 0);
+        transform: translate3d(-100%, 0, 0)
+    }
+}
+
+.fadeOutLeft {
+    -webkit-animation-name: fadeOutLeft;
+    animation-name: fadeOutLeft
+}
+
+@-webkit-keyframes fadeOutLeftBig {
+    0% {
+        opacity: 1
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: translate3d(-2000px, 0, 0);
+        transform: translate3d(-2000px, 0, 0)
+    }
+}
+
+@keyframes fadeOutLeftBig {
+    0% {
+        opacity: 1
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: translate3d(-2000px, 0, 0);
+        transform: translate3d(-2000px, 0, 0)
+    }
+}
+
+.fadeOutLeftBig {
+    -webkit-animation-name: fadeOutLeftBig;
+    animation-name: fadeOutLeftBig
+}
+
+@-webkit-keyframes fadeOutRight {
+    0% {
+        opacity: 1
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: translate3d(100%, 0, 0);
+        transform: translate3d(100%, 0, 0)
+    }
+}
+
+@keyframes fadeOutRight {
+    0% {
+        opacity: 1
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: translate3d(100%, 0, 0);
+        transform: translate3d(100%, 0, 0)
+    }
+}
+
+.fadeOutRight {
+    -webkit-animation-name: fadeOutRight;
+    animation-name: fadeOutRight
+}
+
+@-webkit-keyframes fadeOutRightBig {
+    0% {
+        opacity: 1
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: translate3d(2000px, 0, 0);
+        transform: translate3d(2000px, 0, 0)
+    }
+}
+
+@keyframes fadeOutRightBig {
+    0% {
+        opacity: 1
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: translate3d(2000px, 0, 0);
+        transform: translate3d(2000px, 0, 0)
+    }
+}
+
+.fadeOutRightBig {
+    -webkit-animation-name: fadeOutRightBig;
+    animation-name: fadeOutRightBig
+}
+
+@-webkit-keyframes fadeOutUp {
+    0% {
+        opacity: 1
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: translate3d(0, -100%, 0);
+        transform: translate3d(0, -100%, 0)
+    }
+}
+
+@keyframes fadeOutUp {
+    0% {
+        opacity: 1
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: translate3d(0, -100%, 0);
+        transform: translate3d(0, -100%, 0)
+    }
+}
+
+.fadeOutUp {
+    -webkit-animation-name: fadeOutUp;
+    animation-name: fadeOutUp
+}
+
+@-webkit-keyframes fadeOutUpBig {
+    0% {
+        opacity: 1
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: translate3d(0, -2000px, 0);
+        transform: translate3d(0, -2000px, 0)
+    }
+}
+
+@keyframes fadeOutUpBig {
+    0% {
+        opacity: 1
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: translate3d(0, -2000px, 0);
+        transform: translate3d(0, -2000px, 0)
+    }
+}
+
+.fadeOutUpBig {
+    -webkit-animation-name: fadeOutUpBig;
+    animation-name: fadeOutUpBig
+}
+
+@-webkit-keyframes flip {
+    0% {
+        -webkit-transform: perspective(400px) rotateY(-1turn);
+        transform: perspective(400px) rotateY(-1turn)
+    }
+    0%, 40% {
+        -webkit-animation-timing-function: ease-out;
+        animation-timing-function: ease-out
+    }
+    40% {
+        -webkit-transform: perspective(400px) translateZ(150px) rotateY(-190deg);
+        transform: perspective(400px) translateZ(150px) rotateY(-190deg)
+    }
+    50% {
+        -webkit-transform: perspective(400px) translateZ(150px) rotateY(-170deg);
+        transform: perspective(400px) translateZ(150px) rotateY(-170deg)
+    }
+    50%, 80% {
+        -webkit-animation-timing-function: ease-in;
+        animation-timing-function: ease-in
+    }
+    80% {
+        -webkit-transform: perspective(400px) scale3d(.95, .95, .95);
+        transform: perspective(400px) scale3d(.95, .95, .95)
+    }
+    to {
+        -webkit-transform: perspective(400px);
+        transform: perspective(400px);
+        -webkit-animation-timing-function: ease-in;
+        animation-timing-function: ease-in
+    }
+}
+
+@keyframes flip {
+    0% {
+        -webkit-transform: perspective(400px) rotateY(-1turn);
+        transform: perspective(400px) rotateY(-1turn)
+    }
+    0%, 40% {
+        -webkit-animation-timing-function: ease-out;
+        animation-timing-function: ease-out
+    }
+    40% {
+        -webkit-transform: perspective(400px) translateZ(150px) rotateY(-190deg);
+        transform: perspective(400px) translateZ(150px) rotateY(-190deg)
+    }
+    50% {
+        -webkit-transform: perspective(400px) translateZ(150px) rotateY(-170deg);
+        transform: perspective(400px) translateZ(150px) rotateY(-170deg)
+    }
+    50%, 80% {
+        -webkit-animation-timing-function: ease-in;
+        animation-timing-function: ease-in
+    }
+    80% {
+        -webkit-transform: perspective(400px) scale3d(.95, .95, .95);
+        transform: perspective(400px) scale3d(.95, .95, .95)
+    }
+    to {
+        -webkit-transform: perspective(400px);
+        transform: perspective(400px);
+        -webkit-animation-timing-function: ease-in;
+        animation-timing-function: ease-in
+    }
+}
+
+.animated.flip {
+    -webkit-backface-visibility: visible;
+    backface-visibility: visible;
+    -webkit-animation-name: flip;
+    animation-name: flip
+}
+
+@-webkit-keyframes flipInX {
+    0% {
+        -webkit-transform: perspective(400px) rotateX(90deg);
+        transform: perspective(400px) rotateX(90deg);
+        opacity: 0
+    }
+    0%, 40% {
+        -webkit-animation-timing-function: ease-in;
+        animation-timing-function: ease-in
+    }
+    40% {
+        -webkit-transform: perspective(400px) rotateX(-20deg);
+        transform: perspective(400px) rotateX(-20deg)
+    }
+    60% {
+        -webkit-transform: perspective(400px) rotateX(10deg);
+        transform: perspective(400px) rotateX(10deg);
+        opacity: 1
+    }
+    80% {
+        -webkit-transform: perspective(400px) rotateX(-5deg);
+        transform: perspective(400px) rotateX(-5deg)
+    }
+    to {
+        -webkit-transform: perspective(400px);
+        transform: perspective(400px)
+    }
+}
+
+@keyframes flipInX {
+    0% {
+        -webkit-transform: perspective(400px) rotateX(90deg);
+        transform: perspective(400px) rotateX(90deg);
+        opacity: 0
+    }
+    0%, 40% {
+        -webkit-animation-timing-function: ease-in;
+        animation-timing-function: ease-in
+    }
+    40% {
+        -webkit-transform: perspective(400px) rotateX(-20deg);
+        transform: perspective(400px) rotateX(-20deg)
+    }
+    60% {
+        -webkit-transform: perspective(400px) rotateX(10deg);
+        transform: perspective(400px) rotateX(10deg);
+        opacity: 1
+    }
+    80% {
+        -webkit-transform: perspective(400px) rotateX(-5deg);
+        transform: perspective(400px) rotateX(-5deg)
+    }
+    to {
+        -webkit-transform: perspective(400px);
+        transform: perspective(400px)
+    }
+}
+
+.flipInX {
+    -webkit-backface-visibility: visible !important;
+    backface-visibility: visible !important;
+    -webkit-animation-name: flipInX;
+    animation-name: flipInX
+}
+
+@-webkit-keyframes flipInY {
+    0% {
+        -webkit-transform: perspective(400px) rotateY(90deg);
+        transform: perspective(400px) rotateY(90deg);
+        opacity: 0
+    }
+    0%, 40% {
+        -webkit-animation-timing-function: ease-in;
+        animation-timing-function: ease-in
+    }
+    40% {
+        -webkit-transform: perspective(400px) rotateY(-20deg);
+        transform: perspective(400px) rotateY(-20deg)
+    }
+    60% {
+        -webkit-transform: perspective(400px) rotateY(10deg);
+        transform: perspective(400px) rotateY(10deg);
+        opacity: 1
+    }
+    80% {
+        -webkit-transform: perspective(400px) rotateY(-5deg);
+        transform: perspective(400px) rotateY(-5deg)
+    }
+    to {
+        -webkit-transform: perspective(400px);
+        transform: perspective(400px)
+    }
+}
+
+@keyframes flipInY {
+    0% {
+        -webkit-transform: perspective(400px) rotateY(90deg);
+        transform: perspective(400px) rotateY(90deg);
+        opacity: 0
+    }
+    0%, 40% {
+        -webkit-animation-timing-function: ease-in;
+        animation-timing-function: ease-in
+    }
+    40% {
+        -webkit-transform: perspective(400px) rotateY(-20deg);
+        transform: perspective(400px) rotateY(-20deg)
+    }
+    60% {
+        -webkit-transform: perspective(400px) rotateY(10deg);
+        transform: perspective(400px) rotateY(10deg);
+        opacity: 1
+    }
+    80% {
+        -webkit-transform: perspective(400px) rotateY(-5deg);
+        transform: perspective(400px) rotateY(-5deg)
+    }
+    to {
+        -webkit-transform: perspective(400px);
+        transform: perspective(400px)
+    }
+}
+
+.flipInY {
+    -webkit-backface-visibility: visible !important;
+    backface-visibility: visible !important;
+    -webkit-animation-name: flipInY;
+    animation-name: flipInY
+}
+
+@-webkit-keyframes flipOutX {
+    0% {
+        -webkit-transform: perspective(400px);
+        transform: perspective(400px)
+    }
+    30% {
+        -webkit-transform: perspective(400px) rotateX(-20deg);
+        transform: perspective(400px) rotateX(-20deg);
+        opacity: 1
+    }
+    to {
+        -webkit-transform: perspective(400px) rotateX(90deg);
+        transform: perspective(400px) rotateX(90deg);
+        opacity: 0
+    }
+}
+
+@keyframes flipOutX {
+    0% {
+        -webkit-transform: perspective(400px);
+        transform: perspective(400px)
+    }
+    30% {
+        -webkit-transform: perspective(400px) rotateX(-20deg);
+        transform: perspective(400px) rotateX(-20deg);
+        opacity: 1
+    }
+    to {
+        -webkit-transform: perspective(400px) rotateX(90deg);
+        transform: perspective(400px) rotateX(90deg);
+        opacity: 0
+    }
+}
+
+.flipOutX {
+    -webkit-animation-name: flipOutX;
+    animation-name: flipOutX;
+    -webkit-backface-visibility: visible !important;
+    backface-visibility: visible !important
+}
+
+@-webkit-keyframes flipOutY {
+    0% {
+        -webkit-transform: perspective(400px);
+        transform: perspective(400px)
+    }
+    30% {
+        -webkit-transform: perspective(400px) rotateY(-15deg);
+        transform: perspective(400px) rotateY(-15deg);
+        opacity: 1
+    }
+    to {
+        -webkit-transform: perspective(400px) rotateY(90deg);
+        transform: perspective(400px) rotateY(90deg);
+        opacity: 0
+    }
+}
+
+@keyframes flipOutY {
+    0% {
+        -webkit-transform: perspective(400px);
+        transform: perspective(400px)
+    }
+    30% {
+        -webkit-transform: perspective(400px) rotateY(-15deg);
+        transform: perspective(400px) rotateY(-15deg);
+        opacity: 1
+    }
+    to {
+        -webkit-transform: perspective(400px) rotateY(90deg);
+        transform: perspective(400px) rotateY(90deg);
+        opacity: 0
+    }
+}
+
+.flipOutY {
+    -webkit-backface-visibility: visible !important;
+    backface-visibility: visible !important;
+    -webkit-animation-name: flipOutY;
+    animation-name: flipOutY
+}
+
+@-webkit-keyframes lightSpeedIn {
+    0% {
+        -webkit-transform: translate3d(100%, 0, 0) skewX(-30deg);
+        transform: translate3d(100%, 0, 0) skewX(-30deg);
+        opacity: 0
+    }
+    60% {
+        -webkit-transform: skewX(20deg);
+        transform: skewX(20deg)
+    }
+    60%, 80% {
+        opacity: 1
+    }
+    80% {
+        -webkit-transform: skewX(-5deg);
+        transform: skewX(-5deg)
+    }
+    to {
+        -webkit-transform: none;
+        transform: none;
+        opacity: 1
+    }
+}
+
+@keyframes lightSpeedIn {
+    0% {
+        -webkit-transform: translate3d(100%, 0, 0) skewX(-30deg);
+        transform: translate3d(100%, 0, 0) skewX(-30deg);
+        opacity: 0
+    }
+    60% {
+        -webkit-transform: skewX(20deg);
+        transform: skewX(20deg)
+    }
+    60%, 80% {
+        opacity: 1
+    }
+    80% {
+        -webkit-transform: skewX(-5deg);
+        transform: skewX(-5deg)
+    }
+    to {
+        -webkit-transform: none;
+        transform: none;
+        opacity: 1
+    }
+}
+
+.lightSpeedIn {
+    -webkit-animation-name: lightSpeedIn;
+    animation-name: lightSpeedIn;
+    -webkit-animation-timing-function: ease-out;
+    animation-timing-function: ease-out
+}
+
+@-webkit-keyframes lightSpeedOut {
+    0% {
+        opacity: 1
+    }
+    to {
+        -webkit-transform: translate3d(100%, 0, 0) skewX(30deg);
+        transform: translate3d(100%, 0, 0) skewX(30deg);
+        opacity: 0
+    }
+}
+
+@keyframes lightSpeedOut {
+    0% {
+        opacity: 1
+    }
+    to {
+        -webkit-transform: translate3d(100%, 0, 0) skewX(30deg);
+        transform: translate3d(100%, 0, 0) skewX(30deg);
+        opacity: 0
+    }
+}
+
+.lightSpeedOut {
+    -webkit-animation-name: lightSpeedOut;
+    animation-name: lightSpeedOut;
+    -webkit-animation-timing-function: ease-in;
+    animation-timing-function: ease-in
+}
+
+@-webkit-keyframes rotateIn {
+    0% {
+        transform-origin: center;
+        -webkit-transform: rotate(-200deg);
+        transform: rotate(-200deg);
+        opacity: 0
+    }
+    0%, to {
+        -webkit-transform-origin: center
+    }
+    to {
+        transform-origin: center;
+        -webkit-transform: none;
+        transform: none;
+        opacity: 1
+    }
+}
+
+@keyframes rotateIn {
+    0% {
+        transform-origin: center;
+        -webkit-transform: rotate(-200deg);
+        transform: rotate(-200deg);
+        opacity: 0
+    }
+    0%, to {
+        -webkit-transform-origin: center
+    }
+    to {
+        transform-origin: center;
+        -webkit-transform: none;
+        transform: none;
+        opacity: 1
+    }
+}
+
+.rotateIn {
+    -webkit-animation-name: rotateIn;
+    animation-name: rotateIn
+}
+
+@-webkit-keyframes rotateInDownLeft {
+    0% {
+        transform-origin: left bottom;
+        -webkit-transform: rotate(-45deg);
+        transform: rotate(-45deg);
+        opacity: 0
+    }
+    0%, to {
+        -webkit-transform-origin: left bottom
+    }
+    to {
+        transform-origin: left bottom;
+        -webkit-transform: none;
+        transform: none;
+        opacity: 1
+    }
+}
+
+@keyframes rotateInDownLeft {
+    0% {
+        transform-origin: left bottom;
+        -webkit-transform: rotate(-45deg);
+        transform: rotate(-45deg);
+        opacity: 0
+    }
+    0%, to {
+        -webkit-transform-origin: left bottom
+    }
+    to {
+        transform-origin: left bottom;
+        -webkit-transform: none;
+        transform: none;
+        opacity: 1
+    }
+}
+
+.rotateInDownLeft {
+    -webkit-animation-name: rotateInDownLeft;
+    animation-name: rotateInDownLeft
+}
+
+@-webkit-keyframes rotateInDownRight {
+    0% {
+        transform-origin: right bottom;
+        -webkit-transform: rotate(45deg);
+        transform: rotate(45deg);
+        opacity: 0
+    }
+    0%, to {
+        -webkit-transform-origin: right bottom
+    }
+    to {
+        transform-origin: right bottom;
+        -webkit-transform: none;
+        transform: none;
+        opacity: 1
+    }
+}
+
+@keyframes rotateInDownRight {
+    0% {
+        transform-origin: right bottom;
+        -webkit-transform: rotate(45deg);
+        transform: rotate(45deg);
+        opacity: 0
+    }
+    0%, to {
+        -webkit-transform-origin: right bottom
+    }
+    to {
+        transform-origin: right bottom;
+        -webkit-transform: none;
+        transform: none;
+        opacity: 1
+    }
+}
+
+.rotateInDownRight {
+    -webkit-animation-name: rotateInDownRight;
+    animation-name: rotateInDownRight
+}
+
+@-webkit-keyframes rotateInUpLeft {
+    0% {
+        transform-origin: left bottom;
+        -webkit-transform: rotate(45deg);
+        transform: rotate(45deg);
+        opacity: 0
+    }
+    0%, to {
+        -webkit-transform-origin: left bottom
+    }
+    to {
+        transform-origin: left bottom;
+        -webkit-transform: none;
+        transform: none;
+        opacity: 1
+    }
+}
+
+@keyframes rotateInUpLeft {
+    0% {
+        transform-origin: left bottom;
+        -webkit-transform: rotate(45deg);
+        transform: rotate(45deg);
+        opacity: 0
+    }
+    0%, to {
+        -webkit-transform-origin: left bottom
+    }
+    to {
+        transform-origin: left bottom;
+        -webkit-transform: none;
+        transform: none;
+        opacity: 1
+    }
+}
+
+.rotateInUpLeft {
+    -webkit-animation-name: rotateInUpLeft;
+    animation-name: rotateInUpLeft
+}
+
+@-webkit-keyframes rotateInUpRight {
+    0% {
+        transform-origin: right bottom;
+        -webkit-transform: rotate(-90deg);
+        transform: rotate(-90deg);
+        opacity: 0
+    }
+    0%, to {
+        -webkit-transform-origin: right bottom
+    }
+    to {
+        transform-origin: right bottom;
+        -webkit-transform: none;
+        transform: none;
+        opacity: 1
+    }
+}
+
+@keyframes rotateInUpRight {
+    0% {
+        transform-origin: right bottom;
+        -webkit-transform: rotate(-90deg);
+        transform: rotate(-90deg);
+        opacity: 0
+    }
+    0%, to {
+        -webkit-transform-origin: right bottom
+    }
+    to {
+        transform-origin: right bottom;
+        -webkit-transform: none;
+        transform: none;
+        opacity: 1
+    }
+}
+
+.rotateInUpRight {
+    -webkit-animation-name: rotateInUpRight;
+    animation-name: rotateInUpRight
+}
+
+@-webkit-keyframes rotateOut {
+    0% {
+        transform-origin: center;
+        opacity: 1
+    }
+    0%, to {
+        -webkit-transform-origin: center
+    }
+    to {
+        transform-origin: center;
+        -webkit-transform: rotate(200deg);
+        transform: rotate(200deg);
+        opacity: 0
+    }
+}
+
+@keyframes rotateOut {
+    0% {
+        transform-origin: center;
+        opacity: 1
+    }
+    0%, to {
+        -webkit-transform-origin: center
+    }
+    to {
+        transform-origin: center;
+        -webkit-transform: rotate(200deg);
+        transform: rotate(200deg);
+        opacity: 0
+    }
+}
+
+.rotateOut {
+    -webkit-animation-name: rotateOut;
+    animation-name: rotateOut
+}
+
+@-webkit-keyframes rotateOutDownLeft {
+    0% {
+        transform-origin: left bottom;
+        opacity: 1
+    }
+    0%, to {
+        -webkit-transform-origin: left bottom
+    }
+    to {
+        transform-origin: left bottom;
+        -webkit-transform: rotate(45deg);
+        transform: rotate(45deg);
+        opacity: 0
+    }
+}
+
+@keyframes rotateOutDownLeft {
+    0% {
+        transform-origin: left bottom;
+        opacity: 1
+    }
+    0%, to {
+        -webkit-transform-origin: left bottom
+    }
+    to {
+        transform-origin: left bottom;
+        -webkit-transform: rotate(45deg);
+        transform: rotate(45deg);
+        opacity: 0
+    }
+}
+
+.rotateOutDownLeft {
+    -webkit-animation-name: rotateOutDownLeft;
+    animation-name: rotateOutDownLeft
+}
+
+@-webkit-keyframes rotateOutDownRight {
+    0% {
+        transform-origin: right bottom;
+        opacity: 1
+    }
+    0%, to {
+        -webkit-transform-origin: right bottom
+    }
+    to {
+        transform-origin: right bottom;
+        -webkit-transform: rotate(-45deg);
+        transform: rotate(-45deg);
+        opacity: 0
+    }
+}
+
+@keyframes rotateOutDownRight {
+    0% {
+        transform-origin: right bottom;
+        opacity: 1
+    }
+    0%, to {
+        -webkit-transform-origin: right bottom
+    }
+    to {
+        transform-origin: right bottom;
+        -webkit-transform: rotate(-45deg);
+        transform: rotate(-45deg);
+        opacity: 0
+    }
+}
+
+.rotateOutDownRight {
+    -webkit-animation-name: rotateOutDownRight;
+    animation-name: rotateOutDownRight
+}
+
+@-webkit-keyframes rotateOutUpLeft {
+    0% {
+        transform-origin: left bottom;
+        opacity: 1
+    }
+    0%, to {
+        -webkit-transform-origin: left bottom
+    }
+    to {
+        transform-origin: left bottom;
+        -webkit-transform: rotate(-45deg);
+        transform: rotate(-45deg);
+        opacity: 0
+    }
+}
+
+@keyframes rotateOutUpLeft {
+    0% {
+        transform-origin: left bottom;
+        opacity: 1
+    }
+    0%, to {
+        -webkit-transform-origin: left bottom
+    }
+    to {
+        transform-origin: left bottom;
+        -webkit-transform: rotate(-45deg);
+        transform: rotate(-45deg);
+        opacity: 0
+    }
+}
+
+.rotateOutUpLeft {
+    -webkit-animation-name: rotateOutUpLeft;
+    animation-name: rotateOutUpLeft
+}
+
+@-webkit-keyframes rotateOutUpRight {
+    0% {
+        transform-origin: right bottom;
+        opacity: 1
+    }
+    0%, to {
+        -webkit-transform-origin: right bottom
+    }
+    to {
+        transform-origin: right bottom;
+        -webkit-transform: rotate(90deg);
+        transform: rotate(90deg);
+        opacity: 0
+    }
+}
+
+@keyframes rotateOutUpRight {
+    0% {
+        transform-origin: right bottom;
+        opacity: 1
+    }
+    0%, to {
+        -webkit-transform-origin: right bottom
+    }
+    to {
+        transform-origin: right bottom;
+        -webkit-transform: rotate(90deg);
+        transform: rotate(90deg);
+        opacity: 0
+    }
+}
+
+.rotateOutUpRight {
+    -webkit-animation-name: rotateOutUpRight;
+    animation-name: rotateOutUpRight
+}
+
+@-webkit-keyframes hinge {
+    0% {
+        transform-origin: top left
+    }
+    0%, 20%, 60% {
+        -webkit-transform-origin: top left;
+        -webkit-animation-timing-function: ease-in-out;
+        animation-timing-function: ease-in-out
+    }
+    20%, 60% {
+        -webkit-transform: rotate(80deg);
+        transform: rotate(80deg);
+        transform-origin: top left
+    }
+    40%, 80% {
+        -webkit-transform: rotate(60deg);
+        transform: rotate(60deg);
+        -webkit-transform-origin: top left;
+        transform-origin: top left;
+        -webkit-animation-timing-function: ease-in-out;
+        animation-timing-function: ease-in-out;
+        opacity: 1
+    }
+    to {
+        -webkit-transform: translate3d(0, 700px, 0);
+        transform: translate3d(0, 700px, 0);
+        opacity: 0
+    }
+}
+
+@keyframes hinge {
+    0% {
+        transform-origin: top left
+    }
+    0%, 20%, 60% {
+        -webkit-transform-origin: top left;
+        -webkit-animation-timing-function: ease-in-out;
+        animation-timing-function: ease-in-out
+    }
+    20%, 60% {
+        -webkit-transform: rotate(80deg);
+        transform: rotate(80deg);
+        transform-origin: top left
+    }
+    40%, 80% {
+        -webkit-transform: rotate(60deg);
+        transform: rotate(60deg);
+        -webkit-transform-origin: top left;
+        transform-origin: top left;
+        -webkit-animation-timing-function: ease-in-out;
+        animation-timing-function: ease-in-out;
+        opacity: 1
+    }
+    to {
+        -webkit-transform: translate3d(0, 700px, 0);
+        transform: translate3d(0, 700px, 0);
+        opacity: 0
+    }
+}
+
+.hinge {
+    -webkit-animation-name: hinge;
+    animation-name: hinge
+}
+
+@-webkit-keyframes rollIn {
+    0% {
+        opacity: 0;
+        -webkit-transform: translate3d(-100%, 0, 0) rotate(-120deg);
+        transform: translate3d(-100%, 0, 0) rotate(-120deg)
+    }
+    to {
+        opacity: 1;
+        -webkit-transform: none;
+        transform: none
+    }
+}
+
+@keyframes rollIn {
+    0% {
+        opacity: 0;
+        -webkit-transform: translate3d(-100%, 0, 0) rotate(-120deg);
+        transform: translate3d(-100%, 0, 0) rotate(-120deg)
+    }
+    to {
+        opacity: 1;
+        -webkit-transform: none;
+        transform: none
+    }
+}
+
+.rollIn {
+    -webkit-animation-name: rollIn;
+    animation-name: rollIn
+}
+
+@-webkit-keyframes rollOut {
+    0% {
+        opacity: 1
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: translate3d(100%, 0, 0) rotate(120deg);
+        transform: translate3d(100%, 0, 0) rotate(120deg)
+    }
+}
+
+@keyframes rollOut {
+    0% {
+        opacity: 1
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: translate3d(100%, 0, 0) rotate(120deg);
+        transform: translate3d(100%, 0, 0) rotate(120deg)
+    }
+}
+
+.rollOut {
+    -webkit-animation-name: rollOut;
+    animation-name: rollOut
+}
+
+@-webkit-keyframes zoomIn {
+    0% {
+        opacity: 0;
+        -webkit-transform: scale3d(.3, .3, .3);
+        transform: scale3d(.3, .3, .3)
+    }
+    50% {
+        opacity: 1
+    }
+}
+
+@keyframes zoomIn {
+    0% {
+        opacity: 0;
+        -webkit-transform: scale3d(.3, .3, .3);
+        transform: scale3d(.3, .3, .3)
+    }
+    50% {
+        opacity: 1
+    }
+}
+
+.zoomIn {
+    -webkit-animation-name: zoomIn;
+    animation-name: zoomIn
+}
+
+@-webkit-keyframes zoomInDown {
+    0% {
+        opacity: 0;
+        -webkit-transform: scale3d(.1, .1, .1) translate3d(0, -1000px, 0);
+        transform: scale3d(.1, .1, .1) translate3d(0, -1000px, 0);
+        -webkit-animation-timing-function: cubic-bezier(.55, .055, .675, .19);
+        animation-timing-function: cubic-bezier(.55, .055, .675, .19)
+    }
+    60% {
+        opacity: 1;
+        -webkit-transform: scale3d(.475, .475, .475) translate3d(0, 60px, 0);
+        transform: scale3d(.475, .475, .475) translate3d(0, 60px, 0);
+        -webkit-animation-timing-function: cubic-bezier(.175, .885, .32, 1);
+        animation-timing-function: cubic-bezier(.175, .885, .32, 1)
+    }
+}
+
+@keyframes zoomInDown {
+    0% {
+        opacity: 0;
+        -webkit-transform: scale3d(.1, .1, .1) translate3d(0, -1000px, 0);
+        transform: scale3d(.1, .1, .1) translate3d(0, -1000px, 0);
+        -webkit-animation-timing-function: cubic-bezier(.55, .055, .675, .19);
+        animation-timing-function: cubic-bezier(.55, .055, .675, .19)
+    }
+    60% {
+        opacity: 1;
+        -webkit-transform: scale3d(.475, .475, .475) translate3d(0, 60px, 0);
+        transform: scale3d(.475, .475, .475) translate3d(0, 60px, 0);
+        -webkit-animation-timing-function: cubic-bezier(.175, .885, .32, 1);
+        animation-timing-function: cubic-bezier(.175, .885, .32, 1)
+    }
+}
+
+.zoomInDown {
+    -webkit-animation-name: zoomInDown;
+    animation-name: zoomInDown
+}
+
+@-webkit-keyframes zoomInLeft {
+    0% {
+        opacity: 0;
+        -webkit-transform: scale3d(.1, .1, .1) translate3d(-1000px, 0, 0);
+        transform: scale3d(.1, .1, .1) translate3d(-1000px, 0, 0);
+        -webkit-animation-timing-function: cubic-bezier(.55, .055, .675, .19);
+        animation-timing-function: cubic-bezier(.55, .055, .675, .19)
+    }
+    60% {
+        opacity: 1;
+        -webkit-transform: scale3d(.475, .475, .475) translate3d(10px, 0, 0);
+        transform: scale3d(.475, .475, .475) translate3d(10px, 0, 0);
+        -webkit-animation-timing-function: cubic-bezier(.175, .885, .32, 1);
+        animation-timing-function: cubic-bezier(.175, .885, .32, 1)
+    }
+}
+
+@keyframes zoomInLeft {
+    0% {
+        opacity: 0;
+        -webkit-transform: scale3d(.1, .1, .1) translate3d(-1000px, 0, 0);
+        transform: scale3d(.1, .1, .1) translate3d(-1000px, 0, 0);
+        -webkit-animation-timing-function: cubic-bezier(.55, .055, .675, .19);
+        animation-timing-function: cubic-bezier(.55, .055, .675, .19)
+    }
+    60% {
+        opacity: 1;
+        -webkit-transform: scale3d(.475, .475, .475) translate3d(10px, 0, 0);
+        transform: scale3d(.475, .475, .475) translate3d(10px, 0, 0);
+        -webkit-animation-timing-function: cubic-bezier(.175, .885, .32, 1);
+        animation-timing-function: cubic-bezier(.175, .885, .32, 1)
+    }
+}
+
+.zoomInLeft {
+    -webkit-animation-name: zoomInLeft;
+    animation-name: zoomInLeft
+}
+
+@-webkit-keyframes zoomInRight {
+    0% {
+        opacity: 0;
+        -webkit-transform: scale3d(.1, .1, .1) translate3d(1000px, 0, 0);
+        transform: scale3d(.1, .1, .1) translate3d(1000px, 0, 0);
+        -webkit-animation-timing-function: cubic-bezier(.55, .055, .675, .19);
+        animation-timing-function: cubic-bezier(.55, .055, .675, .19)
+    }
+    60% {
+        opacity: 1;
+        -webkit-transform: scale3d(.475, .475, .475) translate3d(-10px, 0, 0);
+        transform: scale3d(.475, .475, .475) translate3d(-10px, 0, 0);
+        -webkit-animation-timing-function: cubic-bezier(.175, .885, .32, 1);
+        animation-timing-function: cubic-bezier(.175, .885, .32, 1)
+    }
+}
+
+@keyframes zoomInRight {
+    0% {
+        opacity: 0;
+        -webkit-transform: scale3d(.1, .1, .1) translate3d(1000px, 0, 0);
+        transform: scale3d(.1, .1, .1) translate3d(1000px, 0, 0);
+        -webkit-animation-timing-function: cubic-bezier(.55, .055, .675, .19);
+        animation-timing-function: cubic-bezier(.55, .055, .675, .19)
+    }
+    60% {
+        opacity: 1;
+        -webkit-transform: scale3d(.475, .475, .475) translate3d(-10px, 0, 0);
+        transform: scale3d(.475, .475, .475) translate3d(-10px, 0, 0);
+        -webkit-animation-timing-function: cubic-bezier(.175, .885, .32, 1);
+        animation-timing-function: cubic-bezier(.175, .885, .32, 1)
+    }
+}
+
+.zoomInRight {
+    -webkit-animation-name: zoomInRight;
+    animation-name: zoomInRight
+}
+
+@-webkit-keyframes zoomInUp {
+    0% {
+        opacity: 0;
+        -webkit-transform: scale3d(.1, .1, .1) translate3d(0, 1000px, 0);
+        transform: scale3d(.1, .1, .1) translate3d(0, 1000px, 0);
+        -webkit-animation-timing-function: cubic-bezier(.55, .055, .675, .19);
+        animation-timing-function: cubic-bezier(.55, .055, .675, .19)
+    }
+    60% {
+        opacity: 1;
+        -webkit-transform: scale3d(.475, .475, .475) translate3d(0, -60px, 0);
+        transform: scale3d(.475, .475, .475) translate3d(0, -60px, 0);
+        -webkit-animation-timing-function: cubic-bezier(.175, .885, .32, 1);
+        animation-timing-function: cubic-bezier(.175, .885, .32, 1)
+    }
+}
+
+@keyframes zoomInUp {
+    0% {
+        opacity: 0;
+        -webkit-transform: scale3d(.1, .1, .1) translate3d(0, 1000px, 0);
+        transform: scale3d(.1, .1, .1) translate3d(0, 1000px, 0);
+        -webkit-animation-timing-function: cubic-bezier(.55, .055, .675, .19);
+        animation-timing-function: cubic-bezier(.55, .055, .675, .19)
+    }
+    60% {
+        opacity: 1;
+        -webkit-transform: scale3d(.475, .475, .475) translate3d(0, -60px, 0);
+        transform: scale3d(.475, .475, .475) translate3d(0, -60px, 0);
+        -webkit-animation-timing-function: cubic-bezier(.175, .885, .32, 1);
+        animation-timing-function: cubic-bezier(.175, .885, .32, 1)
+    }
+}
+
+.zoomInUp {
+    -webkit-animation-name: zoomInUp;
+    animation-name: zoomInUp
+}
+
+@-webkit-keyframes zoomOut {
+    0% {
+        opacity: 1
+    }
+    50% {
+        -webkit-transform: scale3d(.3, .3, .3);
+        transform: scale3d(.3, .3, .3)
+    }
+    50%, to {
+        opacity: 0
+    }
+}
+
+@keyframes zoomOut {
+    0% {
+        opacity: 1
+    }
+    50% {
+        -webkit-transform: scale3d(.3, .3, .3);
+        transform: scale3d(.3, .3, .3)
+    }
+    50%, to {
+        opacity: 0
+    }
+}
+
+.zoomOut {
+    -webkit-animation-name: zoomOut;
+    animation-name: zoomOut
+}
+
+@-webkit-keyframes zoomOutDown {
+    40% {
+        opacity: 1;
+        -webkit-transform: scale3d(.475, .475, .475) translate3d(0, -60px, 0);
+        transform: scale3d(.475, .475, .475) translate3d(0, -60px, 0);
+        -webkit-animation-timing-function: cubic-bezier(.55, .055, .675, .19);
+        animation-timing-function: cubic-bezier(.55, .055, .675, .19)
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: scale3d(.1, .1, .1) translate3d(0, 2000px, 0);
+        transform: scale3d(.1, .1, .1) translate3d(0, 2000px, 0);
+        -webkit-transform-origin: center bottom;
+        transform-origin: center bottom;
+        -webkit-animation-timing-function: cubic-bezier(.175, .885, .32, 1);
+        animation-timing-function: cubic-bezier(.175, .885, .32, 1)
+    }
+}
+
+@keyframes zoomOutDown {
+    40% {
+        opacity: 1;
+        -webkit-transform: scale3d(.475, .475, .475) translate3d(0, -60px, 0);
+        transform: scale3d(.475, .475, .475) translate3d(0, -60px, 0);
+        -webkit-animation-timing-function: cubic-bezier(.55, .055, .675, .19);
+        animation-timing-function: cubic-bezier(.55, .055, .675, .19)
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: scale3d(.1, .1, .1) translate3d(0, 2000px, 0);
+        transform: scale3d(.1, .1, .1) translate3d(0, 2000px, 0);
+        -webkit-transform-origin: center bottom;
+        transform-origin: center bottom;
+        -webkit-animation-timing-function: cubic-bezier(.175, .885, .32, 1);
+        animation-timing-function: cubic-bezier(.175, .885, .32, 1)
+    }
+}
+
+.zoomOutDown {
+    -webkit-animation-name: zoomOutDown;
+    animation-name: zoomOutDown
+}
+
+@-webkit-keyframes zoomOutLeft {
+    40% {
+        opacity: 1;
+        -webkit-transform: scale3d(.475, .475, .475) translate3d(42px, 0, 0);
+        transform: scale3d(.475, .475, .475) translate3d(42px, 0, 0)
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: scale(.1) translate3d(-2000px, 0, 0);
+        transform: scale(.1) translate3d(-2000px, 0, 0);
+        -webkit-transform-origin: left center;
+        transform-origin: left center
+    }
+}
+
+@keyframes zoomOutLeft {
+    40% {
+        opacity: 1;
+        -webkit-transform: scale3d(.475, .475, .475) translate3d(42px, 0, 0);
+        transform: scale3d(.475, .475, .475) translate3d(42px, 0, 0)
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: scale(.1) translate3d(-2000px, 0, 0);
+        transform: scale(.1) translate3d(-2000px, 0, 0);
+        -webkit-transform-origin: left center;
+        transform-origin: left center
+    }
+}
+
+.zoomOutLeft {
+    -webkit-animation-name: zoomOutLeft;
+    animation-name: zoomOutLeft
+}
+
+@-webkit-keyframes zoomOutRight {
+    40% {
+        opacity: 1;
+        -webkit-transform: scale3d(.475, .475, .475) translate3d(-42px, 0, 0);
+        transform: scale3d(.475, .475, .475) translate3d(-42px, 0, 0)
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: scale(.1) translate3d(2000px, 0, 0);
+        transform: scale(.1) translate3d(2000px, 0, 0);
+        -webkit-transform-origin: right center;
+        transform-origin: right center
+    }
+}
+
+@keyframes zoomOutRight {
+    40% {
+        opacity: 1;
+        -webkit-transform: scale3d(.475, .475, .475) translate3d(-42px, 0, 0);
+        transform: scale3d(.475, .475, .475) translate3d(-42px, 0, 0)
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: scale(.1) translate3d(2000px, 0, 0);
+        transform: scale(.1) translate3d(2000px, 0, 0);
+        -webkit-transform-origin: right center;
+        transform-origin: right center
+    }
+}
+
+.zoomOutRight {
+    -webkit-animation-name: zoomOutRight;
+    animation-name: zoomOutRight
+}
+
+@-webkit-keyframes zoomOutUp {
+    40% {
+        opacity: 1;
+        -webkit-transform: scale3d(.475, .475, .475) translate3d(0, 60px, 0);
+        transform: scale3d(.475, .475, .475) translate3d(0, 60px, 0);
+        -webkit-animation-timing-function: cubic-bezier(.55, .055, .675, .19);
+        animation-timing-function: cubic-bezier(.55, .055, .675, .19)
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: scale3d(.1, .1, .1) translate3d(0, -2000px, 0);
+        transform: scale3d(.1, .1, .1) translate3d(0, -2000px, 0);
+        -webkit-transform-origin: center bottom;
+        transform-origin: center bottom;
+        -webkit-animation-timing-function: cubic-bezier(.175, .885, .32, 1);
+        animation-timing-function: cubic-bezier(.175, .885, .32, 1)
+    }
+}
+
+@keyframes zoomOutUp {
+    40% {
+        opacity: 1;
+        -webkit-transform: scale3d(.475, .475, .475) translate3d(0, 60px, 0);
+        transform: scale3d(.475, .475, .475) translate3d(0, 60px, 0);
+        -webkit-animation-timing-function: cubic-bezier(.55, .055, .675, .19);
+        animation-timing-function: cubic-bezier(.55, .055, .675, .19)
+    }
+    to {
+        opacity: 0;
+        -webkit-transform: scale3d(.1, .1, .1) translate3d(0, -2000px, 0);
+        transform: scale3d(.1, .1, .1) translate3d(0, -2000px, 0);
+        -webkit-transform-origin: center bottom;
+        transform-origin: center bottom;
+        -webkit-animation-timing-function: cubic-bezier(.175, .885, .32, 1);
+        animation-timing-function: cubic-bezier(.175, .885, .32, 1)
+    }
+}
+
+.zoomOutUp {
+    -webkit-animation-name: zoomOutUp;
+    animation-name: zoomOutUp
+}
+
+@-webkit-keyframes slideInDown {
+    0% {
+        -webkit-transform: translate3d(0, -100%, 0);
+        transform: translate3d(0, -100%, 0);
+        visibility: visible
+    }
+    to {
+        -webkit-transform: translateZ(0);
+        transform: translateZ(0)
+    }
+}
+
+@keyframes slideInDown {
+    0% {
+        -webkit-transform: translate3d(0, -100%, 0);
+        transform: translate3d(0, -100%, 0);
+        visibility: visible
+    }
+    to {
+        -webkit-transform: translateZ(0);
+        transform: translateZ(0)
+    }
+}
+
+.slideInDown {
+    -webkit-animation-name: slideInDown;
+    animation-name: slideInDown
+}
+
+@-webkit-keyframes slideInLeft {
+    0% {
+        -webkit-transform: translate3d(-100%, 0, 0);
+        transform: translate3d(-100%, 0, 0);
+        visibility: visible
+    }
+    to {
+        -webkit-transform: translateZ(0);
+        transform: translateZ(0)
+    }
+}
+
+@keyframes slideInLeft {
+    0% {
+        -webkit-transform: translate3d(-100%, 0, 0);
+        transform: translate3d(-100%, 0, 0);
+        visibility: visible
+    }
+    to {
+        -webkit-transform: translateZ(0);
+        transform: translateZ(0)
+    }
+}
+
+.slideInLeft {
+    -webkit-animation-name: slideInLeft;
+    animation-name: slideInLeft
+}
+
+@-webkit-keyframes slideInRight {
+    0% {
+        -webkit-transform: translate3d(100%, 0, 0);
+        transform: translate3d(100%, 0, 0);
+        visibility: visible
+    }
+    to {
+        -webkit-transform: translateZ(0);
+        transform: translateZ(0)
+    }
+}
+
+@keyframes slideInRight {
+    0% {
+        -webkit-transform: translate3d(100%, 0, 0);
+        transform: translate3d(100%, 0, 0);
+        visibility: visible
+    }
+    to {
+        -webkit-transform: translateZ(0);
+        transform: translateZ(0)
+    }
+}
+
+.slideInRight {
+    -webkit-animation-name: slideInRight;
+    animation-name: slideInRight
+}
+
+@-webkit-keyframes slideInUp {
+    0% {
+        -webkit-transform: translate3d(0, 100%, 0);
+        transform: translate3d(0, 100%, 0);
+        visibility: visible
+    }
+    to {
+        -webkit-transform: translateZ(0);
+        transform: translateZ(0)
+    }
+}
+
+@keyframes slideInUp {
+    0% {
+        -webkit-transform: translate3d(0, 100%, 0);
+        transform: translate3d(0, 100%, 0);
+        visibility: visible
+    }
+    to {
+        -webkit-transform: translateZ(0);
+        transform: translateZ(0)
+    }
+}
+
+.slideInUp {
+    -webkit-animation-name: slideInUp;
+    animation-name: slideInUp
+}
+
+@-webkit-keyframes slideOutDown {
+    0% {
+        -webkit-transform: translateZ(0);
+        transform: translateZ(0)
+    }
+    to {
+        visibility: hidden;
+        -webkit-transform: translate3d(0, 100%, 0);
+        transform: translate3d(0, 100%, 0)
+    }
+}
+
+@keyframes slideOutDown {
+    0% {
+        -webkit-transform: translateZ(0);
+        transform: translateZ(0)
+    }
+    to {
+        visibility: hidden;
+        -webkit-transform: translate3d(0, 100%, 0);
+        transform: translate3d(0, 100%, 0)
+    }
+}
+
+.slideOutDown {
+    -webkit-animation-name: slideOutDown;
+    animation-name: slideOutDown
+}
+
+@-webkit-keyframes slideOutLeft {
+    0% {
+        -webkit-transform: translateZ(0);
+        transform: translateZ(0)
+    }
+    to {
+        visibility: hidden;
+        -webkit-transform: translate3d(-100%, 0, 0);
+        transform: translate3d(-100%, 0, 0)
+    }
+}
+
+@keyframes slideOutLeft {
+    0% {
+        -webkit-transform: translateZ(0);
+        transform: translateZ(0)
+    }
+    to {
+        visibility: hidden;
+        -webkit-transform: translate3d(-100%, 0, 0);
+        transform: translate3d(-100%, 0, 0)
+    }
+}
+
+.slideOutLeft {
+    -webkit-animation-name: slideOutLeft;
+    animation-name: slideOutLeft
+}
+
+@-webkit-keyframes slideOutRight {
+    0% {
+        -webkit-transform: translateZ(0);
+        transform: translateZ(0)
+    }
+    to {
+        visibility: hidden;
+        -webkit-transform: translate3d(100%, 0, 0);
+        transform: translate3d(100%, 0, 0)
+    }
+}
+
+@keyframes slideOutRight {
+    0% {
+        -webkit-transform: translateZ(0);
+        transform: translateZ(0)
+    }
+    to {
+        visibility: hidden;
+        -webkit-transform: translate3d(100%, 0, 0);
+        transform: translate3d(100%, 0, 0)
+    }
+}
+
+.slideOutRight {
+    -webkit-animation-name: slideOutRight;
+    animation-name: slideOutRight
+}
+
+@-webkit-keyframes slideOutUp {
+    0% {
+        -webkit-transform: translateZ(0);
+        transform: translateZ(0)
+    }
+    to {
+        visibility: hidden;
+        -webkit-transform: translate3d(0, -100%, 0);
+        transform: translate3d(0, -100%, 0)
+    }
+}
+
+@keyframes slideOutUp {
+    0% {
+        -webkit-transform: translateZ(0);
+        transform: translateZ(0)
+    }
+    to {
+        visibility: hidden;
+        -webkit-transform: translate3d(0, -100%, 0);
+        transform: translate3d(0, -100%, 0)
+    }
+}
+
+.slideOutUp {
+    -webkit-animation-name: slideOutUp;
+    animation-name: slideOutUp
+}

+ 48 - 0
src/assets/doctor.svg

@@ -0,0 +1,48 @@
+<svg width="60" height="61" viewBox="0 0 60 61" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="&#229;&#140;&#187;&#231;&#148;&#159;">
+<path id="Vector" d="M30 60C13.43 60 0 46.57 0 30C0 13.43 13.43 0 30 0C46.57 0 60 13.43 60 30C60 46.57 46.57 60 30 60ZM30 4C15.64 4 4 15.64 4 30C4 44.36 15.64 56 30 56C44.36 56 56 44.36 56 30C56 15.64 44.36 4 30 4Z" fill="url(#paint0_linear_2111_3396)"/>
+<g id="Vector_2">
+<mask id="path-2-inside-1_2111_3396" fill="white">
+<path d="M59 30C59 13.98 46.02 1 30 1C13.98 1 1 13.98 1 30C1 46.02 13.98 59 30 59V60C13.43 60 0 46.57 0 30C0 13.43 13.43 0 30 0C46.57 0 60 13.43 60 30C60 46.57 46.57 60 30 60V59C46.02 59 59 46.02 59 30Z"/>
+</mask>
+<path d="M59 30C59 13.98 46.02 1 30 1C13.98 1 1 13.98 1 30C1 46.02 13.98 59 30 59V60C13.43 60 0 46.57 0 30C0 13.43 13.43 0 30 0C46.57 0 60 13.43 60 30C60 46.57 46.57 60 30 60V59C46.02 59 59 46.02 59 30Z" fill="url(#paint1_linear_2111_3396)"/>
+<path d="M30 59H29.5V60H30H30.5V59H30ZM59 30H60C60 13.4277 46.5723 0 30 0V1V2C45.4677 2 58 14.5323 58 30H59ZM30 1V0C13.4277 0 0 13.4277 0 30H1H2C2 14.5323 14.5323 2 30 2V1ZM1 30H0C0 46.5723 13.4277 60 30 60V59V58C14.5323 58 2 45.4677 2 30H1ZM30 60V59C13.9823 59 1 46.0177 1 30H0H-1C-1 47.1223 12.8777 61 30 61V60ZM0 30H1C1 13.9823 13.9823 1 30 1V0V-1C12.8777 -1 -1 12.8777 -1 30H0ZM30 0V1C46.0177 1 59 13.9823 59 30H60H61C61 12.8777 47.1223 -1 30 -1V0ZM60 30H59C59 46.0177 46.0177 59 30 59V60V61C47.1223 61 61 47.1223 61 30H60ZM30 59V60C46.5723 60 60 46.5723 60 30H59H58C58 45.4677 45.4677 58 30 58V59Z" fill="url(#paint2_linear_2111_3396)" mask="url(#path-2-inside-1_2111_3396)"/>
+</g>
+<path id="Vector_3" d="M30 58C14.54 58 2 45.46 2 30C2 14.54 14.54 2 30 2C45.46 2 58 14.54 58 30C58 45.46 45.46 58 30 58ZM30 4C15.64 4 4 15.64 4 30C4 44.36 15.64 56 30 56C44.36 56 56 44.36 56 30C56 15.64 44.36 4 30 4Z" fill="url(#paint3_linear_2111_3396)"/>
+<path id="Vector_4" d="M57 30C57 15.09 44.91 3 30 3C15.09 3 3 15.09 3 30C3 44.91 15.09 57 30 57V58C14.54 58 2 45.46 2 30C2 14.54 14.54 2 30 2C45.46 2 58 14.54 58 30C58 45.46 45.46 58 30 58V57C44.91 57 57 44.91 57 30Z" fill="url(#paint4_linear_2111_3396)"/>
+<path id="Vector_5" d="M18.8499 46H39.0899C39.9999 46 40.8299 46.49 41.2699 47.28L41.3499 47.44L45.5499 56.44C46.3199 58.1 45.1099 60 43.2799 60H15.3699C13.6099 60 12.4099 58.24 13.0399 56.6L16.5099 47.6C16.8799 46.64 17.8099 46 18.8399 46H18.8499Z" fill="url(#paint5_linear_2111_3396)" stroke="url(#paint6_linear_2111_3396)"/>
+<path id="Vector_6" d="M24.6502 51.9299C24.4202 52.2899 24.1602 52.5899 23.8802 52.8399L23.2402 52.2499C23.8202 51.7299 24.2402 51.0999 24.4902 50.3599L25.2602 50.5799C25.1902 50.7899 25.1202 50.9799 25.0402 51.1599H28.4802V51.9299H26.6202V52.3299C26.6002 52.5499 26.5802 52.7599 26.5602 52.9599H29.0202V53.7599H26.3902C26.2602 54.1699 26.0802 54.5299 25.8602 54.8399C25.4102 55.4099 24.6802 55.8499 23.6602 56.1699L23.2002 55.4499C24.1902 55.1399 24.8602 54.7499 25.2302 54.2799C25.3402 54.1199 25.4402 53.9399 25.5102 53.7599H23.2502V52.9599H25.7302C25.7702 52.7499 25.7802 52.5499 25.7902 52.3299V51.9299H24.6502ZM26.7002 54.0999C27.5402 54.4599 28.3002 54.9099 29.0002 55.4399L28.5302 56.1599C27.7902 55.5499 27.0402 55.0699 26.2602 54.7199L26.6902 54.0999H26.7002ZM21.9502 49.4199H29.1602V50.2199H22.7702V56.2999H29.4502V57.1199H21.9502V49.4199ZM32.2602 51.5399C31.9002 52.3499 31.4502 53.0199 30.9002 53.5599L30.3402 52.8499C31.1502 51.9999 31.7002 50.8399 31.9902 49.3899L32.8602 49.5399C32.7702 49.9499 32.6702 50.3399 32.5602 50.6999H34.2302V49.1899H35.0802V50.6999H37.9702V51.5299H35.0802V53.3899H37.7902V54.2199H35.0802V56.2699H38.5502V57.1199H30.5702V56.2699H34.2302V54.2199H31.6602V53.3899H34.2302V51.5299H32.2502L32.2602 51.5399Z" fill="#D38307"/>
+</g>
+<defs>
+<linearGradient id="paint0_linear_2111_3396" x1="21.91" y1="1.82" x2="37.95" y2="57.72" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FFC737"/>
+<stop offset="0.5" stop-color="#FFDD64"/>
+<stop offset="0.98" stop-color="#CF7C00"/>
+</linearGradient>
+<linearGradient id="paint1_linear_2111_3396" x1="13.9568" y1="4.10072" x2="30" y2="60" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FFC737"/>
+<stop offset="0.501018" stop-color="#FFDD64"/>
+<stop offset="0.979167" stop-color="#CF7C00"/>
+</linearGradient>
+<linearGradient id="paint2_linear_2111_3396" x1="30" y1="0" x2="30" y2="60" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FFF5DA"/>
+<stop offset="0.245067" stop-color="white" stop-opacity="0"/>
+</linearGradient>
+<linearGradient id="paint3_linear_2111_3396" x1="21.91" y1="1.82" x2="37.95" y2="57.72" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FFB800"/>
+<stop offset="1" stop-color="#FFE195"/>
+</linearGradient>
+<linearGradient id="paint4_linear_2111_3396" x1="30" y1="2" x2="30" y2="58" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FFD874"/>
+<stop offset="1" stop-color="#FFEDBF"/>
+</linearGradient>
+<linearGradient id="paint5_linear_2111_3396" x1="15.8799" y1="48.99" x2="55.5699" y2="63" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FFE6A8"/>
+<stop offset="1" stop-color="#FFBA08"/>
+</linearGradient>
+<linearGradient id="paint6_linear_2111_3396" x1="34.9999" y1="39.11" x2="30.3899" y2="54.08" gradientUnits="userSpaceOnUse">
+<stop stop-color="white" stop-opacity="0.55"/>
+<stop offset="1" stop-color="white" stop-opacity="0"/>
+</linearGradient>
+</defs>
+</svg>

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 14 - 0
src/assets/guanjia.svg


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 3 - 0
src/assets/icon/iconfont.css


BIN=BIN
src/assets/icon/iconfont.eot


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
src/assets/icon/iconfont.js


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 28 - 0
src/assets/icon/iconfont.svg


BIN=BIN
src/assets/icon/iconfont.ttf


BIN=BIN
src/assets/icon/iconfont.woff


BIN=BIN
src/assets/icon/iconfont.woff2


+ 51 - 0
src/assets/icon/tim.css

@@ -0,0 +1,51 @@
+@font-face {
+  font-family: 'tim';
+  src: url('tim.eot?gollaf');
+  src: url('tim.eot?gollaf#iefix') format('embedded-opentype'),
+      url('tim.ttf?gollaf') format('truetype'),
+      url('tim.woff?gollaf') format('woff'),
+      url('tim.svg?gollaf#tim') format('svg');
+  font-weight: normal;
+  font-style: normal;
+  font-display: block;
+}
+
+[class^="tim-icon-"], [class*=" tim-icon-"] {
+  /* use !important to prevent issues with browser extensions that change fonts */
+  font-family: 'tim' !important;
+  speak: none;
+  font-style: normal;
+  font-weight: normal;
+  font-variant: normal;
+  text-transform: none;
+  line-height: 1;
+
+  /* Better Font Rendering =========== */
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.tim-icon-friend-add:before {
+  content: "\e907";
+}
+.tim-icon-close:before {
+  content: "\e901";
+}
+.tim-icon-right:before {
+  content: "\e903";
+}
+.tim-icon-add:before {
+  content: "\e904";
+}
+.tim-icon-refresh:before {
+  content: "\e905";
+}
+.tim-icon-send:before {
+  content: "\e902";
+}
+.tim-icon-angle:before {
+  content: "\e900";
+}
+.tim-icon-angle-middle:before {
+  content: "\e906";
+}

BIN=BIN
src/assets/icon/tim.eot


+ 18 - 0
src/assets/icon/tim.svg

@@ -0,0 +1,18 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata>Generated by IcoMoon</metadata>
+<defs>
+<font id="tim" horiz-adv-x="1024">
+<font-face units-per-em="1024" ascent="960" descent="-64" />
+<missing-glyph horiz-adv-x="1024" />
+<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
+<glyph unicode="&#xe900;" glyph-name="angle" horiz-adv-x="410" d="M53.893-51.211c5.48 176.067 42.594 341.987 105.815 494.339l-3.415-9.287c67.368 170.846 113.179 269.474 220.968 377.263 14.339 14.217 24.492 32.634 28.461 53.252l0.103 0.643c1.321 5.986 2.078 12.862 2.078 19.914 0 29.579-13.312 56.048-34.27 73.745l-0.144 0.118c-13.613 8.803-30.249 14.035-48.108 14.035-0.708 0-1.415-0.008-2.119-0.025l0.105 0.002h-323.368s0-916.21 0-1024z" />
+<glyph unicode="&#xe901;" glyph-name="close" d="M557.312 459.552l265.28 263.904c12.544 12.48 12.608 32.704 0.128 45.248-12.512 12.576-32.704 12.608-45.248 0.128l-265.344-263.936-263.040 263.84c-12.448 12.48-32.704 12.544-45.248 0.064-12.512-12.48-12.544-32.736-0.064-45.28l262.976-263.776-265.152-263.744c-12.544-12.48-12.608-32.704-0.128-45.248 6.24-6.272 14.464-9.44 22.688-9.44 8.16 0 16.32 3.104 22.56 9.312l265.216 263.808 265.44-266.24c6.24-6.272 14.432-9.408 22.656-9.408 8.192 0 16.352 3.136 22.592 9.344 12.512 12.48 12.544 32.704 0.064 45.248l-265.376 266.176z" />
+<glyph unicode="&#xe902;" glyph-name="send" d="M350.912 367.424l-299.968 182.72 908.48 355.648-159.072-818.272-387.040 247.872 345.952 373.472zM416 268.8v-209.92l128.256 130.208z" />
+<glyph unicode="&#xe903;" glyph-name="right" d="M822.464 707.456c-5.192 5.738-12.666 9.328-20.979 9.328-8.918 0-16.871-4.131-22.050-10.585l-0.043-0.055-352.96-417.664-181.92 212.992c-5.228 6.441-13.144 10.523-22.014 10.523-8.368 0-15.887-3.633-21.066-9.409l-0.023-0.027c-5.835-6.529-9.401-15.192-9.401-24.689 0-8.977 3.187-17.211 8.491-23.63l-0.050 0.063 204.096-238.944c5.76-6.752 13.696-10.56 22.016-10.56h0.096c8.877 0.144 16.768 4.243 22.008 10.606l0.040 0.050 374.976 443.744c11.52 13.728 11.008 35.328-1.216 48.256z" />
+<glyph unicode="&#xe904;" glyph-name="add" d="M512 140.8c17.673 0 32 14.327 32 32v0 256h256c17.673 0 32 14.327 32 32s-14.327 32-32 32v0h-256v256c0 17.673-14.327 32-32 32s-32-14.327-32-32v0-256h-256c-17.673 0-32-14.327-32-32s14.327-32 32-32v0h256v-256c0-17.673 14.327-32 32-32v0z" />
+<glyph unicode="&#xe905;" glyph-name="refresh" d="M832 476.8c-17.673 0-32-14.327-32-32v0c0-158.784-129.216-288-288-288s-288 129.216-288 288 129.216 288 288 288c66.208 0 129.536-22.752 180.608-64h-84.608c-17.673 0-32-14.327-32-32s14.327-32 32-32v0h160c17.673 0 32 14.327 32 32v0 160c0 17.673-14.327 32-32 32s-32-14.327-32-32v0-80.96c-60.256 50.365-138.555 80.951-223.998 80.96h-0.002c-194.080 0-352-157.92-352-352s157.92-352 352-352 352 157.92 352 352c0 17.673-14.327 32-32 32v0z" />
+<glyph unicode="&#xe906;" glyph-name="angle-middle" d="M361.945-51.199c5.48 176.063 42.593 341.979 105.813 494.327l-3.415-9.287c67.366 170.842 113.176 269.467 220.963 377.255 14.339 14.217 24.491 32.633 28.46 53.251l0.103 0.643c1.321 5.986 2.078 12.862 2.078 19.914 0 29.578-13.312 56.047-34.269 73.743l-0.144 0.118c-13.613 8.803-30.248 14.035-48.107 14.035-0.708 0-1.415-0.008-2.119-0.025l0.105 0.002h-323.361s0-916.191 0-1023.977z" />
+<glyph unicode="&#xe907;" glyph-name="friend-add" d="M781.713 596.267c0 150.519-120.415 270.933-270.933 270.933s-270.933-120.415-270.933-270.933c0-99.342 54.187-189.653 138.477-234.809-114.394-45.156-201.695-144.498-225.778-270.933-3.010-15.052 6.021-33.114 24.083-36.124h6.021c15.052 0 27.093 9.031 30.104 24.083 27.093 141.487 150.519 243.84 295.016 246.85h6.021c147.508 0 267.923 123.425 267.923 270.933zM300.054 596.267c0 117.404 93.321 210.726 210.726 210.726s210.726-93.321 210.726-210.726c0-114.394-93.321-207.716-207.716-210.726h-9.031c-114.394 6.021-204.705 99.342-204.705 210.726zM841.921 207.929h-60.207v60.207c0 18.062-12.041 30.104-30.104 30.104s-30.104-12.041-30.104-30.104v-60.207h-60.207c-18.062 0-30.104-12.041-30.104-30.104s12.041-30.104 30.104-30.104h60.207v-60.207c0-18.062 12.041-30.104 30.104-30.104s30.104 12.041 30.104 30.104v60.207h60.207c18.062 0 30.104 12.041 30.104 30.104s-12.041 30.104-30.104 30.104z" />
+</font></defs></svg>

BIN=BIN
src/assets/icon/tim.ttf


BIN=BIN
src/assets/icon/tim.woff


+ 1 - 0
src/assets/image/add-friend.png

@@ -0,0 +1 @@
+placeholder

+ 1 - 0
src/assets/image/back.png

@@ -0,0 +1 @@
+placeholder

+ 1 - 0
src/assets/image/big-camera-off.png

@@ -0,0 +1 @@
+placeholder

+ 1 - 0
src/assets/image/big-camera-on.png

@@ -0,0 +1 @@
+placeholder

+ 1 - 0
src/assets/image/big-mic-off.png

@@ -0,0 +1 @@
+placeholder

+ 1 - 0
src/assets/image/big-mic-on.png

@@ -0,0 +1 @@
+placeholder

+ 1 - 0
src/assets/image/camera-open.png

@@ -0,0 +1 @@
+placeholder

+ 1 - 0
src/assets/image/close-mic.png

@@ -0,0 +1 @@
+placeholder

+ 1 - 0
src/assets/image/close-relay.png

@@ -0,0 +1 @@
+placeholder

+ 1 - 0
src/assets/image/close.png

@@ -0,0 +1 @@
+placeholder

+ 1 - 0
src/assets/image/hangup.png

@@ -0,0 +1 @@
+placeholder

+ 1 - 0
src/assets/image/live-icon-gray.png

@@ -0,0 +1 @@
+placeholder

+ 1 - 0
src/assets/image/live-icon-hover.png

@@ -0,0 +1 @@
+placeholder

+ 1 - 0
src/assets/image/live-icon.png

@@ -0,0 +1 @@
+placeholder

+ 1 - 0
src/assets/image/living-icon.gif

@@ -0,0 +1 @@
+placeholder

+ 1 - 0
src/assets/image/logo.png

@@ -0,0 +1 @@
+placeholder

+ 1 - 0
src/assets/image/merger-relay.png

@@ -0,0 +1 @@
+placeholder

+ 1 - 0
src/assets/image/no-video.png

@@ -0,0 +1 @@
+placeholder

+ 1 - 0
src/assets/image/open-mic.png

@@ -0,0 +1 @@
+placeholder

+ 1 - 0
src/assets/image/pause-icon.png

@@ -0,0 +1 @@
+placeholder

+ 1 - 0
src/assets/image/poster.png

@@ -0,0 +1 @@
+placeholder

+ 1 - 0
src/assets/image/sig-relay.png

@@ -0,0 +1 @@
+placeholder

+ 1 - 0
src/assets/image/video-bg.png

@@ -0,0 +1 @@
+placeholder

+ 1 - 0
src/assets/image/web-pusher-start.png

@@ -0,0 +1 @@
+placeholder

+ 1 - 0
src/assets/image/web-pusher-stop.png

@@ -0,0 +1 @@
+placeholder

+ 56 - 0
src/assets/qunliao.svg

@@ -0,0 +1,56 @@
+<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="&#231;&#190;&#164;">
+<path id="Vector" d="M30 59.5C46.2939 59.5 59.5 46.2939 59.5 30C59.5 13.7061 46.2939 0.5 30 0.5C13.7061 0.5 0.5 13.7061 0.5 30C0.5 46.2939 13.7061 59.5 30 59.5ZM30 56.5C15.3639 56.5 3.5 44.6361 3.5 30C3.5 15.3639 15.3639 3.5 30 3.5C44.6361 3.5 56.5 15.3639 56.5 30C56.5 44.6361 44.6361 56.5 30 56.5Z" fill="url(#paint0_linear_3215_4406)" stroke="url(#paint1_linear_3215_4406)"/>
+<g id="Vector_2">
+<mask id="path-2-inside-1_3215_4406" fill="white">
+<path d="M59 30C59 13.98 46.02 1 30 1C13.98 1 1 13.98 1 30C1 46.02 13.98 59 30 59V60C13.43 60 0 46.57 0 30C0 13.43 13.43 0 30 0C46.57 0 60 13.43 60 30C60 46.57 46.57 60 30 60V59C46.02 59 59 46.02 59 30Z"/>
+</mask>
+<path d="M59 30C59 13.98 46.02 1 30 1C13.98 1 1 13.98 1 30C1 46.02 13.98 59 30 59V60C13.43 60 0 46.57 0 30C0 13.43 13.43 0 30 0C46.57 0 60 13.43 60 30C60 46.57 46.57 60 30 60V59C46.02 59 59 46.02 59 30Z" fill="url(#paint2_linear_3215_4406)"/>
+<path d="M30 59H29.5V60H30H30.5V59H30ZM59 30H60C60 13.4277 46.5723 0 30 0V1V2C45.4677 2 58 14.5323 58 30H59ZM30 1V0C13.4277 0 0 13.4277 0 30H1H2C2 14.5323 14.5323 2 30 2V1ZM1 30H0C0 46.5723 13.4277 60 30 60V59V58C14.5323 58 2 45.4677 2 30H1ZM30 60V59C13.9823 59 1 46.0177 1 30H0H-1C-1 47.1223 12.8777 61 30 61V60ZM0 30H1C1 13.9823 13.9823 1 30 1V0V-1C12.8777 -1 -1 12.8777 -1 30H0ZM30 0V1C46.0177 1 59 13.9823 59 30H60H61C61 12.8777 47.1223 -1 30 -1V0ZM60 30H59C59 46.0177 46.0177 59 30 59V60V61C47.1223 61 61 47.1223 61 30H60ZM30 59V60C46.5723 60 60 46.5723 60 30H59H58C58 45.4677 45.4677 58 30 58V59Z" fill="url(#paint3_linear_3215_4406)" mask="url(#path-2-inside-1_3215_4406)"/>
+</g>
+<path id="Vector_3" d="M30 58C14.54 58 2 45.46 2 30C2 14.54 14.54 2 30 2C45.46 2 58 14.54 58 30C58 45.46 45.46 58 30 58ZM30 4C15.64 4 4 15.64 4 30C4 44.36 15.64 56 30 56C44.36 56 56 44.36 56 30C56 15.64 44.36 4 30 4Z" fill="url(#paint4_linear_3215_4406)"/>
+<path id="Vector_4" d="M57 30C57 15.09 44.91 3 30 3C15.09 3 3 15.09 3 30C3 44.91 15.09 57 30 57V58C14.54 58 2 45.46 2 30C2 14.54 14.54 2 30 2C45.46 2 58 14.54 58 30C58 45.46 45.46 58 30 58V57C44.91 57 57 44.91 57 30Z" fill="url(#paint5_linear_3215_4406)"/>
+<g id="Group 9009">
+<path id="Rectangle 42" d="M18.8486 45.5H39.0898C40.0608 45.5002 40.9437 46.0626 41.3545 46.9424L45.5547 55.9424C46.3282 57.5998 45.1181 59.5 43.2891 59.5H15.374C13.6182 59.5 12.4096 57.7376 13.042 56.0996L16.5166 47.0996C16.8887 46.1358 17.8155 45.5001 18.8486 45.5Z" fill="url(#paint6_linear_3215_4406)" stroke="url(#paint7_linear_3215_4406)"/>
+<path id="&#231;&#190;&#164;_2" d="M28.969 52.85H27.124C27.061 53.084 26.989 53.318 26.917 53.534H29.212V56.855H28.456V56.369H27.25V56.873H26.494V54.515C26.278 54.92 26.026 55.271 25.747 55.577L25.297 54.866C25.756 54.362 26.107 53.696 26.341 52.85H25.72V52.121H26.512C26.557 51.842 26.602 51.554 26.638 51.266H25.504V50.51H26.701C26.719 50.222 26.728 49.943 26.737 49.655H25.774V48.926H28.969V50.51H29.419V51.266H28.969V52.85ZM27.511 49.655C27.502 49.943 27.493 50.231 27.475 50.51H28.195V49.655H27.511ZM27.412 51.266C27.376 51.563 27.331 51.851 27.286 52.121H28.195V51.266H27.412ZM27.25 55.622H28.456V54.308H27.25V55.622ZM31.156 51.086H29.617V50.267H30.544C30.337 49.79 30.112 49.358 29.878 48.971L30.598 48.629C30.85 49.043 31.093 49.511 31.327 50.015L30.814 50.267H31.678C31.93 49.727 32.146 49.16 32.326 48.557L33.127 48.836C32.947 49.367 32.758 49.844 32.551 50.267H33.415V51.086H31.993V52.184H33.226V52.976H31.993V54.083H33.613V54.911H31.993V56.918H31.156V54.911H29.554V54.083H31.156V52.976H29.833V52.184H31.156V51.086Z" fill="#E95606"/>
+</g>
+</g>
+<defs>
+<linearGradient id="paint0_linear_3215_4406" x1="30" y1="58" x2="30" y2="4.5374e-06" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F75903"/>
+<stop offset="0.501018" stop-color="#FF5C03"/>
+<stop offset="0.979167" stop-color="#E95606"/>
+</linearGradient>
+<linearGradient id="paint1_linear_3215_4406" x1="30" y1="60" x2="30" y2="0" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FFD8C3"/>
+<stop offset="0.245067" stop-color="white" stop-opacity="0"/>
+</linearGradient>
+<linearGradient id="paint2_linear_3215_4406" x1="25" y1="0.5" x2="30" y2="60" gradientUnits="userSpaceOnUse">
+<stop offset="0.0309764" stop-color="#FF8847"/>
+<stop offset="0.979167" stop-color="#E95606"/>
+</linearGradient>
+<linearGradient id="paint3_linear_3215_4406" x1="30" y1="0" x2="30" y2="60" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FFD8C3"/>
+<stop offset="0.245067" stop-color="white" stop-opacity="0"/>
+</linearGradient>
+<linearGradient id="paint4_linear_3215_4406" x1="31" y1="3" x2="31" y2="56" gradientUnits="userSpaceOnUse">
+<stop stop-color="#E95606"/>
+<stop offset="1" stop-color="#FE9D74"/>
+</linearGradient>
+<linearGradient id="paint5_linear_3215_4406" x1="30" y1="2" x2="30" y2="58" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FE9D74"/>
+<stop offset="1" stop-color="#FDD5AD"/>
+</linearGradient>
+<linearGradient id="paint6_linear_3215_4406" x1="17.113" y1="45" x2="56.8062" y2="59.0099" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FFB393"/>
+<stop offset="0.173077" stop-color="#FEBC99"/>
+<stop offset="0.293269" stop-color="#FFE4D5"/>
+<stop offset="0.447115" stop-color="#FFB893"/>
+<stop offset="1" stop-color="#FE772C"/>
+</linearGradient>
+<linearGradient id="paint7_linear_3215_4406" x1="55.7217" y1="45" x2="51.1055" y2="59.9655" gradientUnits="userSpaceOnUse">
+<stop stop-color="white" stop-opacity="0.55"/>
+<stop offset="1" stop-color="white" stop-opacity="0"/>
+</linearGradient>
+</defs>
+</svg>

+ 78 - 0
src/components/ImAvatar/index.vue

@@ -0,0 +1,78 @@
+<template>
+  <span
+    class="im-avatar-root"
+    :style="rootStyle"
+  >
+    <img
+      class="im-avatar-img"
+      :src="displaySrc"
+      alt=""
+      @error="onError"
+    />
+  </span>
+</template>
+
+<script>
+import { IM_AVATAR_DEFAULT, IM_AVATAR_FALLBACK } from '@/utils/imAvatar'
+
+export default {
+  name: 'ImAvatar',
+  props: {
+    src: [String, Number],
+    type: [String, Number],
+    /** 像素边长,固定为圆形容器,避免大图撑破会话/侧栏布局 */
+    size: {
+      type: Number,
+      default: 44,
+    },
+  },
+  data() {
+    return {
+      /** 仅在实际 url 加载失败时置 true,占位图不再二次切换 */
+      loadFailed: false,
+    }
+  },
+  computed: {
+    rootStyle() {
+      const s = this.size
+      return {
+        width: `${s}px`,
+        height: `${s}px`,
+      }
+    },
+    displaySrc() {
+      const raw = this.src != null && this.src !== '' ? String(this.src).trim() : ''
+      if (raw) {
+        return this.loadFailed ? IM_AVATAR_FALLBACK : raw
+      }
+      return IM_AVATAR_DEFAULT
+    },
+  },
+  methods: {
+    onError() {
+      const raw = this.src != null && this.src !== '' ? String(this.src).trim() : ''
+      if (raw) this.loadFailed = true
+    },
+  },
+}
+</script>
+
+<style scoped>
+.im-avatar-root {
+  display: inline-block;
+  vertical-align: middle;
+  flex-shrink: 0;
+  border-radius: 50%;
+  overflow: hidden;
+  line-height: 0;
+  box-sizing: border-box;
+}
+.im-avatar-img {
+  width: 100%;
+  height: 100%;
+  max-width: 100%;
+  max-height: 100%;
+  object-fit: cover;
+  display: block;
+}
+</style>

+ 83 - 0
src/components/blacklist/blacklist-item.vue

@@ -0,0 +1,83 @@
+<template>
+  <div class="blacklist-item-wrapper" @click="resetContent">
+    <avatar :src="profile.avatar" :size="40" />
+    <div class="item">{{profile.nick||profile.userID}}</div>
+    <div  class="cancel-btn" @click="removeFromBlacklist">取消</div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'BlacklistItem',
+  props: {
+    profile: {
+      type: Object,
+      required: true
+    }
+  },
+  methods: {
+    removeFromBlacklist() {
+      this.tim
+        .removeFromBlacklist({ userIDList: [this.profile.userID] })
+        .then(() => {
+          this.$store.commit('removeFromBlacklist', this.profile.userID)
+        })
+        .catch(error => {
+          this.$store.commit('showMessage', {
+            type: 'error',
+            message: error.message
+          })
+        })
+    },
+    resetContent() {
+      this.$store.commit('resetCurrentConversation')
+      this.$store.commit('resetFriendContent')
+      this.$store.commit('resetApplicationContent')
+    }
+  }
+}
+</script>
+
+<style lang="stylus" scoped>
+.item {
+  padding-left: 20px;
+  width: 100%;
+  color: #111;
+  box-sizing: border-box;
+  word-wrap: break-word;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.blacklist-item-wrapper {
+  padding-top 15px
+  padding-bottom 15px
+  display flex
+  align-items center
+  justify-content flex-start
+  position relative
+  &:hover {
+    background-color #f5f5f5
+  }
+  .cancel-btn {
+    position absolute
+    right 5px
+    width 40px
+    height 24px
+    font-size 13px
+    color #ffffff
+    border-radius 12px
+    line-height 24px
+    text-align center
+    background-color #00a4ff
+  }
+}
+
+.avatar {
+  width: 40px;
+  height: 40px;
+  border-radius: 50%;
+  flex-shrink: 0
+  box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.1);
+}
+</style>

+ 44 - 0
src/components/blacklist/blacklist.vue

@@ -0,0 +1,44 @@
+<template>
+  <div class="blacklist-wrapper" :class="{'default': !hasBlacklist}">
+    <div v-if="hasBlacklist">
+      <blacklist-item
+        v-for="item in blacklist"
+        :key="item.userID"
+        :profile="item"
+      />
+    </div>
+    <span style="color:gray" v-else>黑名单还是空的</span>
+  </div>
+</template>
+
+<script>
+import BlacklistItem from './blacklist-item'
+import { mapState } from 'vuex'
+export default {
+  name: 'Blacklist',
+  components: {
+    BlacklistItem
+  },
+  computed: {
+    ...mapState({
+      blacklist: state => state.blacklist.blacklist
+    }),
+    hasBlacklist() {
+      return this.blacklist.length > 0
+    }
+  }
+}
+</script>
+
+<style lang="stylus" scoped>
+.blacklist-wrapper {
+  padding-top 15px
+  overflow-y: scroll;
+}
+.default {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 100%;
+}
+</style>

+ 74 - 39
src/components/conversation/conversation-item.vue

@@ -9,7 +9,7 @@
       </div>
       <div class="warp">
         <div class="avatar-wrapper">
-          <avatar :src="avatar" :type="conversation.faceURL" class="avatar" />
+          <avatar :src="avatar" :type="conversation.faceURL" class="avatar" :size="40" />
           <img v-if="avatarBorder" :src="avatarBorder" class="avatar-border" />
         </div>
         <div class="c-content">
@@ -59,6 +59,7 @@
 import { mapGetters, mapState } from 'vuex'
 import { isToday, getDate, getTime } from '../../utils/date'
 import { getOpenIM } from '@/utils/openIM';
+import { imUserIdEqual } from '@/utils/common'
 import doctorBorder from '@/assets/doctor.svg'
 import guanjiaBorder from '@/assets/guanjia.svg'
 import qunliaoBorder from '@/assets/qunliao.svg'
@@ -175,7 +176,7 @@ export default {
       let text = ''
       switch (this.lastMsg.contentType) {
         case 2101:
-          if (this.lastMsg.sendID === this.$store.getters.userID) {
+          if (imUserIdEqual(this.lastMsg.sendID, this.$store.getters.userID)) {
             return '你撤回了一条消息'
           }
           if (this.lastMsg.sessionType === 1) {
@@ -235,36 +236,50 @@ export default {
     deleteConversation(event) {
       // 停止冒泡,避免和点击会话的事件冲突
       event.stopPropagation()
-      this.OpenIM
-        .deleteConversationAndDeleteAllMsg(this.conversation.conversationID)
+      this.$confirm(
+        '删除会话后将同时删除该会话的全部聊天记录,且无法恢复。是否继续?',
+        '删除会话确认',
+        {
+          confirmButtonText: '删除',
+          cancelButtonText: '取消',
+          type: 'warning',
+          closeOnClickModal: false,
+          distinguishCancelAndClose: true
+        }
+      )
         .then(() => {
-          this.$store.commit('showMessage', {
-            message: `会话【${this.conversationName}】删除成功!`,
-            type: 'success'
-          })
-          this.popoverVisible = false
-          this.$store.commit('resetCurrentConversation')
-          this.OpenIM.getConversationListSplit({
-            offset: 0,
-            count: 100,
-          })
-            .then(({ data }) => {
-              // 调用成功
-              console.log("获取到会话列表",data)
-              this.conversationList= data
-              this.$store.commit('updateConversationList', data)
+          this.OpenIM
+            .deleteConversationAndDeleteAllMsg(this.conversation.conversationID)
+            .then(() => {
+              this.$store.commit('showMessage', {
+                message: `会话【${this.conversationName}】删除成功!`,
+                type: 'success'
+              })
+              this.popoverVisible = false
+              this.$store.commit('resetCurrentConversation')
+              this.OpenIM.getConversationListSplit({
+                offset: 0,
+                count: 100,
+              })
+                .then(({ data }) => {
+                  // 调用成功
+                  console.log("获取到会话列表",data)
+                  this.conversationList= data
+                  this.$store.commit('updateConversationList', data)
+                })
+                .catch(({ errCode, errMsg }) => {
+                  // 调用失败
+                })
             })
-            .catch(({ errCode, errMsg }) => {
-              // 调用失败
+            .catch(error => {
+              this.$store.commit('showMessage', {
+                message: `会话【${this.conversationName}】删除失败!, error=${error.message}`,
+                type: 'error'
+              })
+              this.popoverVisible = false
             })
         })
-        .catch(error => {
-          this.$store.commit('showMessage', {
-            message: `会话【${this.conversationName}】删除失败!, error=${error.message}`,
-            type: 'error'
-          })
-          this.popoverVisible = false
-        })
+        .catch(() => {})
     },
     showContextMenu() {
       this.popoverVisible = true
@@ -295,22 +310,24 @@ export default {
     height 100%
     pointer-events none
 .conversation-item-container
-  padding 15px 20px
+  padding 12px 16px
   cursor pointer
   position relative
   overflow hidden
-  transition .2s
+  transition background-color .15s ease
+  border-bottom 1px solid #e5e5e5
   // &:first-child
   //   padding-top 30px
   &:hover
-    background-color $background
     .close-btn
       right 3px
+  &:hover:not(.choose)
+    background-color #f5f5f5
   .close-btn
     position absolute
     right -20px
     top 3px
-    color $font-dark
+    color #909399
     transition: all .2s ease;
     &:hover
       color $danger
@@ -330,14 +347,14 @@ export default {
       display flex
       line-height 21px
       .name
-        color #f7f7f8
+        color #111
         flex 1
         min-width 0px
 
       .unread-count
         padding-left 10px
         flex-shrink 0
-        color #76828c
+        color #999
         font-size 12px
 
         .badge
@@ -363,7 +380,7 @@ export default {
         flex 1
         overflow hidden
         min-width 0px
-        color: #a5b5c1
+        color #888
 
         .remind
           color #f35f5f
@@ -372,11 +389,29 @@ export default {
         padding-left 10px
         flex-shrink 0
         text-align right
-        color #76828c
+        color #b2b2b2
 
-  .choose {
-    background-color: #404953;
-  }
+  /* 微信 PC:当前会话整行绿色高亮(类在同一 div 上,须用 &.choose) */
+  &.choose
+    background-color #07c160
+    .name
+      color #fff
+    .unread-count
+      color rgba(255, 255, 255, 0.92)
+    .row-2
+      .summary,
+      .summary .text,
+      .date
+        color rgba(255, 255, 255, 0.88)
+      .remind
+        color #fff9c4
+    .unread-count .badge
+      background-color #fff
+      color #07c160
+    .close-btn
+      color rgba(255, 255, 255, 0.75)
+      &:hover
+        color #fff
   .context-menu-button {
     padding: 10px
     border: 2px solid #2d8cf0;

+ 5 - 3
src/components/conversation/conversation-list.vue

@@ -176,15 +176,16 @@ export default {
   .header-bar
     flex-shrink 0
     height 50px
-    border-bottom 1px solid $background-deep-dark
+    background-color #f5f5f5
+    border-bottom 1px solid #e5e5e5
     padding 10px 10px 10px 20px
     button
       float right
       display: inline-block;
       cursor: pointer;
-      background $background-deep-dark
+      background transparent
       border: none
-      color: $font-dark;
+      color: #666;
       box-sizing: border-box;
       transition: .3s;
       -moz-user-select: none;
@@ -208,6 +209,7 @@ export default {
   .scroll-container
     overflow-y scroll
     flex 1
+    background-color #f5f5f5
 .bottom-circle-btn {
   position: absolute;
   bottom: 20px;

+ 1 - 1
src/components/conversation/conversation-selected-list.vue

@@ -10,7 +10,7 @@
             <!--                        <conversation-item :conversation="item"/>-->
             <div class="conversation-item-container">
               <div class="warp">
-                <avatar :src="avatar(conversation)" :type="conversation.type" />
+                <avatar :src="avatar(conversation)" :type="conversation.type" :size="40" />
                 <div class="c-content">
                   <div class="row-1">
                     <div class="name">

+ 1 - 1
src/components/conversation/conversationProfile/add-friend-profile

@@ -20,7 +20,7 @@
                     />
                 </div>
                 <div class="content" v-if="!showEditFaceUrl">
-                    <avatar :src="groupProfile.avatar"/>
+                    <avatar :src="groupProfile.avatar" :size="56" />
 
                 </div>
                 <el-input

+ 1 - 1
src/components/conversation/conversationProfile/group-member-list.vue

@@ -15,7 +15,7 @@
           <popover placement="right" :key="member.userID">
             <group-member-info :member="member" />
             <div slot="reference" class="group-member" @click="currentMemberID = member.userID">
-              <avatar :title=getGroupMemberAvatarText(member.role) :src="member.avatar" />
+              <avatar :title=getGroupMemberAvatarText(member.role) :src="member.avatar" :size="32" />
               <div class="member-name text-ellipsis">
                 <span v-if="member.nameCard" :title=member.nameCard>{{ member.nameCard }}</span>
                 <span v-else-if="member.nick" :title=member.nick>{{ member.nick }}</span>

+ 1 - 1
src/components/conversation/conversationProfile/group-profile.vue

@@ -22,7 +22,7 @@
             <!--            />-->
           </div>
           <div class="content" v-if="!showEditFaceUrl">
-            <avatar :src="groupData.faceURL" />
+            <avatar :src="groupData.faceURL" :size="56" />
           </div>
           <el-input
             ref="editFaceUrl"

+ 1 - 1
src/components/conversation/conversationProfile/user-profile.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="profile-user">
-    <avatar :title=userProfile.userID :src="userProfile.avatar" />
+    <avatar :title=userProfile.userID :src="userProfile.avatar" :size="48" />
     <div class="nick-name text-ellipsis">
       <span v-if="userProfile.nick" :title=userProfile.nick>
         {{ userProfile.nick }}

+ 134 - 41
src/components/conversation/current-conversation.vue

@@ -21,11 +21,11 @@
           </div>
           <div class="no-more" v-else>没有更多了</div>
           <el-checkbox-group v-model="checkList" v-if="selectMessage">
-            <el-checkbox :label="message.clientMsgID" v-for="message in currentMessageList" :key="message.clientMsgID" :disabled="message.status==='fail'">
+            <el-checkbox :label="messageRowKey(message, index)" v-for="(message, index) in currentMessageList" :key="messageRowKey(message, index)" :disabled="message.status==='fail'">
               <message-item   :message="message"/>
             </el-checkbox>
           </el-checkbox-group>
-          <message-item v-else v-for="message in currentMessageList" :key="message.clientMsgID" :message="message">
+          <message-item v-else v-for="(message, index) in currentMessageList" :key="messageRowKey(message, index)" :message="message">
           </message-item>
         </div>
         <div v-show="isShowScrollButtomTips" class="newMessageTips" @click="scrollMessageListToButtom">回到最新位置</div>
@@ -114,6 +114,8 @@
         positionX: 0,
         positionY: 0,
         OpenIM: null,
+        /** 切换会话后延迟滚底(等首屏消息渲染 / 图片占位) */
+        scrollAfterSwitchTimers: [],
       }
     },
     computed: {
@@ -208,6 +210,7 @@
       }
     },
     beforeDestroy() {
+      this.clearScrollAfterSwitchTimers()
       if (this.$refs.dropdown && this.$refs.dropdown.$el) {
         this.$refs.dropdown.$el.removeEventListener('mousedown', this.move)
       }
@@ -223,6 +226,24 @@
       }
     },
     watch: {
+      'currentConversation.conversationID'(newId, oldId) {
+        if (!newId || newId === oldId) {
+          return
+        }
+        this.clearScrollAfterSwitchTimers()
+        this.preScrollHeight = 0
+        this.isShowScrollButtomTips = false
+        this.scheduleScrollToBottomAfterConversationSwitch()
+      },
+      /** 只要消息列表条数增加,就尝试滚底(新消息 / 自己发的消息都会触发) */
+      'currentMessageList.length'(next, prev) {
+        if (next > (prev || 0)) {
+          this.scrollMessageListToButtom()
+          this.$nextTick(() => this.scrollMessageListToButtom())
+          setTimeout(() => this.scrollMessageListToButtom(), 80)
+          setTimeout(() => this.scrollMessageListToButtom(), 300)
+        }
+      },
       currentUnreadCount(next) {
         if (!this.hidden && next > 0) {
           this.OpenIM.markConversationMessageAsRead(this.currentConversation.conversationID)
@@ -293,7 +314,7 @@
       },
       selectedMessage() {
         this.selectedMessageList = this.checkList.map((id) => {
-          return this.currentMessageList.find((item) => item.clientMsgID === id)
+          return this.currentMessageList.find((item, idx) => this.messageRowKey(item, idx) === id)
         }).filter(Boolean)
 
         console.log("checkList:", this.checkList)
@@ -318,8 +339,11 @@
         if (!messageListNode) {
           return
         }
-        // 距离底部20px内强制滚到底部,否则提示有新消息
-        if (this.preScrollHeight - messageListNode.clientHeight - messageListNode.scrollTop < 20) {
+        // preScrollHeight === 0:刚切换会话,避免沿用上一会话高度导致误判为「不在底部」
+        const nearBottom =
+          this.preScrollHeight === 0 ||
+          this.preScrollHeight - messageListNode.clientHeight - messageListNode.scrollTop < 20
+        if (nearBottom) {
           this.$nextTick(() => {
             messageListNode.scrollTop = messageListNode.scrollHeight
           })
@@ -329,6 +353,31 @@
         }
         this.preScrollHeight = messageListNode.scrollHeight
       },
+      clearScrollAfterSwitchTimers() {
+        if (!this.scrollAfterSwitchTimers || !this.scrollAfterSwitchTimers.length) {
+          return
+        }
+        this.scrollAfterSwitchTimers.forEach(id => clearTimeout(id))
+        this.scrollAfterSwitchTimers = []
+      },
+      /** 切换会话后多次尝试滚底,覆盖 getMessageList 异步与 DOM 滞后 */
+      scheduleScrollToBottomAfterConversationSwitch() {
+        const run = () => {
+          const el = this.$refs['message-list']
+          if (!el) {
+            return
+          }
+          el.scrollTop = el.scrollHeight
+          this.preScrollHeight = el.scrollHeight
+          this.isShowScrollButtomTips = false
+        }
+        run()
+        this.$nextTick(run)
+        this.$nextTick(() => this.$nextTick(run))
+        ;[80, 200, 450].forEach(ms => {
+          this.scrollAfterSwitchTimers.push(setTimeout(run, ms))
+        })
+      },
       // 直接滚到底部
       scrollMessageListToButtom() {
         this.$nextTick(() => {
@@ -346,6 +395,15 @@
       },
       onImageLoaded() {
         this.keepMessageListOnButtom()
+      },
+      /** clientMsgID 缺失时不能用 undefined 做 key,否则 Vue 复用错节点,新消息看起来像「没出来」 */
+      messageRowKey(message, index) {
+        if (!message) return `i-${index}`
+        return (
+          message.clientMsgID ||
+          (message.serverMsgID != null ? `s-${message.serverMsgID}` : '') ||
+          `${message.sendID || ''}-${message.recvID || ''}-${message.sendTime || ''}-${message.contentType}-${index}`
+        )
       }
     }
   }
@@ -354,15 +412,26 @@
 <style lang="stylus" scoped>
 /* 当前会话的骨架屏 */
 .current-conversation-wrapper
+  position relative
+  /* 父层(el-col)高度在部分页面不是确定值,100% 会失效,导致输入栏顶到上方 */
   height 80vh
-  background-color #f5f5f5
+  min-height 0
+  /* 微信 PC:主聊天区浅灰底 */
+  background-color #ededed
   color #1c2438
   display flex
+  flex-direction column
+  min-width 0
+  flex 1
   .current-conversation
     display: flex;
     flex-direction: column;
+    flex 1 1 0
+    min-width 0
+    min-height 0
     width: 100%;
-    height: 80vh;
+    height: 100%;
+    overflow hidden
   .profile
     height: $height;
     overflow-y: scroll;
@@ -381,20 +450,32 @@
     padding: 10px 10px;
 
 .header
-  border-bottom 1px solid $border-base
-  height 50px
+  flex-shrink 0
+  height 56px
   position relative
+  display flex
+  align-items center
+  background-color #fff
+  border-bottom 1px solid #e7e7e7
+  box-shadow 0 1px 0 rgba(0, 0, 0, 0.03)
   .name
-    padding 0 20px
-    color $base
-    font-size 18px
-    font-weight bold
-    line-height 50px
-    text-shadow $font-dark 0 0 0.1em
+    flex 1
+    min-width 0
+    padding 0 48px 0 20px
+    color #191919
+    font-size 16px
+    font-weight 600
+    line-height 56px
+    text-align left
+    text-shadow none
+    overflow hidden
+    text-overflow ellipsis
+    white-space nowrap
   .btn-more-info
     position absolute
-    top 10px
-    right -15px
+    top 50%
+    right 12px
+    transform translateY(-50%)
     border-radius 50%
     width 30px
     height 30px
@@ -404,43 +485,51 @@
       right 0
       z-index 0
       content ""
-      width: 15px
-      height: 30px
-      border: 1px solid #e7e7e7;
-      border-radius: 0 100% 100% 0/50%
-      border-left: none
-      background-color #f5f5f5;
+      width 15px
+      height 30px
+      border 1px solid #e3e3e3
+      border-radius 0 100% 100% 0 / 50%
+      border-left none
+      background-color #f7f7f7
     &::after
       content ""
-      width: 8px;
-      height: 8px;
-      transition: transform 0.8s;
-      border-top: 2px solid #a5b5c1;
-      border-right: 2px solid #a5b5c1;
-      float:right;
-      position:relative;
-      top: 11px;
-      right: 8px;
-      transform:rotate(45deg)
+      width 8px
+      height 8px
+      transition transform 0.25s ease, border-color 0.2s
+      border-top 2px solid #8a8a8a
+      border-right 2px solid #8a8a8a
+      position absolute
+      top 11px
+      right 8px
+      transform rotate(45deg)
     &.left-arrow
-      transform rotate(180deg)
+      transform translateY(-50%) rotate(180deg)
       &::before
-        background-color #ffffff;
+        background-color #fff
     &:hover
       &::after
-        border-color #5cadff;
+        border-color #07c160
+      &::before
+        border-color #d0d0d0
 .cc-content
   display: flex;
-  flex 1
+  flex 1 1 0
   flex-direction: column;
-  height: 100%;
+  min-height 0
   overflow: hidden;
   position: relative;
+  background-color #ededed
   .message-list
+    flex 1 1 0
+    min-height 0
     width: 100%;
     box-sizing: border-box;
-    overflow-y: scroll;
-    padding: 0 20px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    -webkit-overflow-scrolling: touch;
+    /* 底部大留白:最后一条气泡 + 时间戳 + 未读标记都必须完整露在输入框上方,微信风格 */
+    padding: 12px 16px 180px;
+    background-color #ededed
   .newMessageTips
     position: absolute
     cursor: pointer;
@@ -457,7 +546,11 @@
     background-color: $white;
     color: $primary;
 .footer
-  border-top: 1px solid $border-base;
+  flex-shrink 0
+  width 100%
+  box-sizing border-box
+  background-color #f5f5f5
+  border-top 1px solid #e5e5e5
   .merger-btn {
     height 150px
     padding 3px 20px 20px 20px

+ 3 - 3
src/components/friend/friend-application/application-item.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="application-item-container" :id="'application-'+ index" @click="selectedItem">
     <div  class="application-box">
-      <avatar :src="application.avatar" />
+      <avatar :src="application.avatar" :size="44" />
       <div class="application-content">
         <span class="application-name text-ellipsis">{{application.nick || application.userID}}</span>
         <span class="application-wording text-ellipsis" v-if="application.wording">{{application.wording}} </span>
@@ -81,7 +81,7 @@ export default {
   }
 }
 </script>
-<style lang="stylus" scpoed>
+<style lang="stylus" scoped>
   .application-item-container {
     padding-left 40px
     .application-box {
@@ -104,7 +104,7 @@ export default {
         .application-name {
           display block
           font-size 16px
-          color #ffffff
+          color #111
           height 20px
           margin-bottom 3px
           line-height 20px

+ 12 - 3
src/components/friend/friend-container.vue

@@ -69,7 +69,7 @@
       </p>
       <div class="sendBtn" @click.stop="checkoutConversation(friendProfile.userID)">发送消息</div>
       <div class="delete-text" v-show="friendType==='groupFriend'" @click.stop="removeFromFriendGroup(friendProfile.userID)">从该群组中移除</div>
-      <div class="delete-text" v-show="friendType==='friendList'" @click.stop="removeFromFriendList(friendProfile.userID)">删除该好友</div>
+      <!-- <div class="delete-text" v-show="friendType==='friendList'" @click.stop="removeFromFriendList(friendProfile.userID)">删除该好友</div> -->
     </div>
     <div class="friend-box" v-if="showApplicationContent">
       <div class="profile-container" >
@@ -399,9 +399,18 @@
   }
 </script>
 
-<style lang="stylus" scpoed>
+<style lang="stylus" scoped>
+  /* 覆盖在会话区域上方,不参与 flex 占位,避免把聊天区挤没后输入条溢出到左侧会话列表 */
   .friend-content {
-    /*width 60%*/
+    position absolute
+    left 0
+    top 0
+    right 0
+    bottom 0
+    z-index 20
+    overflow-y auto
+    background-color #f5f5f5
+    box-sizing border-box
     width 100%
     .friend-box {
       margin 100px auto 0

+ 2 - 2
src/components/friend/friend-item.vue

@@ -5,7 +5,7 @@
     @click="selectedItem"
   >
     <div class="avatar-wrapper">
-      <avatar :src="friend.faceURL" class="avatar" />
+      <avatar :src="friend.faceURL" class="avatar" :size="40" />
       <img v-if="avatarBorder" :src="avatarBorder" class="avatar-border" />
     </div>
 
@@ -150,7 +150,7 @@
   .item-nick
     padding-left 20px
     width 100%
-    color #fff
+    color #111
     box-sizing border-box
     word-wrap break-word
     overflow hidden

+ 9 - 9
src/components/friend/friend-list.vue

@@ -10,7 +10,7 @@
       </el-input>
       <el-divider></el-divider>
       <div class="search-item" v-if="searchShow">
-        <avatar :src="profile.avatar" />
+        <avatar :src="profile.avatar" :size="44" />
         <div   class="item-nick">{{profile.nick||profile.userID}}</div>
         <img  class="add-friend-icon" src="../../assets/image/add-friend.png" @click="addFriendPopClick"/>
       </div>
@@ -20,7 +20,7 @@
       <el-form :model="addForm">
         <el-form-item label="" :label-width="formLabelWidth">
           <div class="search-item">
-            <avatar :src="profile.avatar" />
+            <avatar :src="profile.avatar" :size="44" />
             <div  class="item-nick">{{profile.nick||profile.userID}}</div>
           </div>
         </el-form-item>
@@ -409,7 +409,7 @@
     }
   }
 </script>
-<style lang="stylus" scpoed>
+<style lang="stylus" scoped>
 .friend-container {
     height 100%
     width 100%
@@ -418,12 +418,12 @@
   .add-friend {
     width 100%
     cursor pointer
-    color #dddddd
+    color #666
     font-size 18px
     margin 20px auto 0px
     text-align center
     height 40px
-    border-bottom 1px solid #1c2438
+    border-bottom 1px solid #e5e5e5
   }
   .scroll-container  {
     overflow-y scroll
@@ -459,7 +459,7 @@
       }
       .el-submenu__title, .el-menu-item {
         font-size 16px
-        color #ffffff
+        color #111
       }
       .el-menu-item {
         padding-left 20px !important
@@ -477,10 +477,10 @@
         /*padding-left 0 !important*/
       }
       .el-menu-item:focus, .el-menu-item:hover {
-        background-color #404953
+        background-color #f0f0f0
       }
       .el-submenu__title:focus, .el-submenu__title:hover {
-        background-color #404953
+        background-color #f0f0f0
       }
       .el-menu-item {
         padding-left 10px !important
@@ -497,7 +497,7 @@
       }
     }
     .friend-list-container {
-      border-top 1px solid #1c2438
+      border-top 1px solid #e5e5e5
       padding-top 10px
       padding-left -15px
     }

+ 444 - 0
src/components/group-live/components/live-chat.vue

@@ -0,0 +1,444 @@
+<template>
+    <div class="comment-wrapper">
+      <div class="message-list" ref="message-list">
+        <template v-for="(item, index) in avChatRoomMessageList">
+          <!-- 进群提示消息 -->
+          <template v-if="item.type === 'TIMGroupTipElem' && item.payload.groupJoinType === 1">
+            <div class="msg-item-join" :key="`join_msg_item_${index}`">{{`欢迎${item.nick || item.payload.userIDList.join('')}进入直播间`}}</div>
+          </template>
+          <!-- 退群提示消息 -->
+          <template v-if="item.type === 'TIMGroupTipElem' && item.payload.groupJoinType === 0">
+            <div class="msg-item-join" :key="`leave_msg_item_${index}`">{{`${item.nick || item.payload.userIDList.join('')}离开了直播间`}}</div>
+          </template>
+          <!-- 普通文本消息 -->
+          <template v-if="item.type === 'TIMTextElem'">
+            <div class="msg-item-text" :key="`text_msg_item_${index}`">
+              <img class="avatar" :src="getAvatar(item)" alt=""/>
+              <p class="nick" >{{getNick(item)}}</p>
+              <p class="msg" ><span class="msg-text">{{ item.payload.text }}</span></p>
+            </div>
+          </template>
+          <template v-if="item.type === 'TIMCustomElem'">
+            <!-- native 弹幕消息 -->
+            <template v-if="item.payload.data.command === '5'">
+              <div class="msg-item-text" :key="`barrage_msg_item_${index}`">
+              <img class="avatar" :src="getAvatar(item)" alt=""/>
+              <p class="nick" >{{getNick(item)}}</p>
+              <p class="msg" ><span class="msg-text">{{ item.payload.data.message }}</span></p>
+            </div>
+            </template>
+            <!-- 礼物消息 -->
+            <template v-if="item.payload.data.command === '6'">
+              <div class="msg-item-gift" :key="`gift_msg_item_${index}`">
+                <img class="avatar" :src="getAvatar(item)" alt=""/>
+                <p class="nick" >{{getNick(item)}}</p>
+                <p class="msg" >送了一个{{ giftInfo[item.payload.data.message - 1].name }}</p>
+                <img class="gift-pic" :src="giftInfo[item.payload.data.message - 1].icon" alt="" />
+              </div>
+            </template>
+          </template>
+        </template>
+      </div>
+      <live-gift  v-if="!isAnchor" />
+      <div class="send-container">
+        <textarea
+          class="comment-message"
+          :placeholder="`${sendAvailable ? '请输入内容...' : '开始直播后可以互动聊天哦!'}`"
+          v-model="messageContent"
+          :disabled="!sendAvailable"
+          @keyup.enter="sendMessage"
+        ></textarea>
+        <el-tooltip
+          class="item"
+          effect="dark"
+          content="按Enter发送消息"
+          placement="left-start"
+        >
+           <div class="btn-send" @click="sendMessage">
+            <div class="tim-icon-send"></div>
+          </div>
+        </el-tooltip>
+      </div>
+    </div>
+</template>
+
+<script>
+import { mapState } from 'vuex'
+import { Tooltip } from 'element-ui'
+import liveGift from './live-gift'
+
+export default {
+  name: 'liveChat',
+  data() {
+    return {
+      sendAvailable: false,
+      messageContent: '',
+      defaultAvatar: 'https://imgcache.qq.com/open/qcloud/video/act/webim-avatar/avatar-2.png',
+      giftInfo: [
+        {
+          icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590482989_25.png',
+          name: '火箭'
+        },
+        {
+          icon: 'https://8.url.cn/huayang/resource/now/new_gift/1507876726_3',
+          name: '鸡蛋'
+        },
+        {
+          icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590482294_7.png',
+          name: '吻'
+        },
+        {
+          icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590482461_11.png',
+          name: '跑车'
+        },
+        {
+          icon: 'https://8.url.cn/huayang/resource/now/new_gift/1594714453_7.png',
+          name: '嘉年华'
+        },
+        {
+          icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590482754_17.png',
+          name: '玫瑰'
+        },
+        {
+          icon: 'https://8.url.cn/huayang/resource/now/new_gift/1594281297_11.png',
+          name: '直升机'
+        },
+        {
+          icon: 'https://8.url.cn/huayang/resource/now/new_gift/1507876472_1',
+          name: '点赞'
+        },
+        {
+          icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590483038_27.png',
+          name: '比心'
+        },
+        {
+          icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590483168_31.png',
+          name: '冰淇淋'
+        },
+        {
+          icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590483225_33.png',
+          name: '玩偶'
+        },
+        {
+          icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590483278_35.png',
+          name: '蛋糕'
+        },
+        {
+          icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590483348_37.png',
+          name: '豪华轿车'
+        },
+        {
+          icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590483429_39.png',
+          name: '游艇'
+        },
+        {
+          icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590483505_41.png',
+          name: '翅膀'
+        }
+      ]
+    }
+  },
+  computed: {
+    ...mapState({
+      user: state => state.imuser,
+      groupLiveInfo: state => state.groupLive.groupLiveInfo,
+      avChatRoomMessageList: state => {
+        const list = state.groupLive.avChatRoomMessageList
+        list.map((item) => {
+          if (item.type === 'TIMCustomElem' && typeof item.payload.data === 'string' && item.payload.data.indexOf('{') > -1) {
+            item.payload.data = JSON.parse(item.payload.data)
+          }
+          return item
+        })
+        return list
+      },
+      // avChatRoomBarrageMessageList: state => state.groupLive.avChatRoomBarrageMessageList,
+      // avChatRoomGiftMessageList: state => state.groupLive.avChatRoomGiftMessageList,
+    }),
+    isAnchor () {
+      return this.user.userID === this.groupLiveInfo.anchorID
+    }
+  },
+  components: {
+    liveGift,
+    ElTooltip: Tooltip,
+  },
+  created() {
+    // 此处监听,防止观众侧派发加群事件时,该组件中还未注册监听
+    this.$bus.$on('join-group-live-avchatroom', () => {
+      this.joinGroupLiveAvChatRoom()
+    })
+  },
+  mounted() {
+    this.$bus.$on('group-live-send-gift', (index)=> {
+      const message = this.tim.createCustomMessage({
+        to: this.groupLiveInfo.roomID,
+        conversationType: 3,
+        payload: {
+          data: JSON.stringify({version: '1.0.0' ,'message': `${index}`,'command':'6','action':301}),
+          description: '',
+          extension: ''
+        }
+      })
+      // 此处用JSON序列化和反序列化对message断链
+      this.$store.commit('pushAvChatRoomMessageList', JSON.parse(JSON.stringify(message)))
+      this.tim.sendMessage(message).catch(error => {
+        this.$store.commit('showMessage', {
+          type: 'error',
+          message: error.message
+        })
+      })
+    })
+  },
+  beforeDestroy() {
+    this.$bus.$off('join-group-live-avchatroom')
+    this.$bus.$off('group-live-send-gift')
+    if (!this.isAnchor && this.groupLiveInfo.isNeededQuitRoom === 1) {
+      this.quitGroupLiveAvChatRoom()
+    }
+    this.$store.commit('updateGroupLiveInfo', { isNeededQuitRoom: 0 })
+  },
+  methods: {
+    getAvatar (item) {
+      if (item.from === this.user.userID) {
+        return this.user.currentUserProfile.avatar || this.defaultAvatar
+      }
+      return item.avatar || this.defaultAvatar
+    },
+    getNick (item) {
+      if (item.from === this.user.userID) {
+        return this.user.currentUserProfile.nick || item.from
+      }
+      return item.nick || item.from
+    },
+    // 进入直播互动群
+    joinGroupLiveAvChatRoom() {
+      this.tim.joinGroup({
+        groupID: this.groupLiveInfo.roomID
+      }).then((imResponse) => {
+        const status = imResponse.data.status
+        if (status === this.OpenIM.TYPES.JOIN_STATUS_SUCCESS || status === this.OpenIM.TYPES.JOIN_STATUS_ALREADY_IN_GROUP) {
+          this.sendAvailable = true
+        }
+      }).catch(() => {})
+    },
+    // 退出直播互动群
+    quitGroupLiveAvChatRoom() {
+      this.tim.quitGroup(this.groupLiveInfo.roomID).then(() => {}).catch(() => {})
+    },
+    sendMessage() {
+      if (!this.sendAvailable) {
+        this.$store.commit('showMessage', {
+          message: '开始直播后可以互动聊天哦!',
+          type: 'warning'
+        })
+        return
+      }
+      if (this.messageContent === '' || this.messageContent.trim().length === 0) {
+        this.messageContent = ''
+        this.$store.commit('showMessage', {
+          message: '不能发送空消息哦!',
+          type: 'info'
+        })
+        return
+      }
+      const message = this.tim.createTextMessage({
+        to: this.groupLiveInfo.roomID,
+        conversationType: 3,
+        payload: { text: this.messageContent }
+      })
+      // 此处用JSON序列化和反序列化对message断链
+      this.$store.commit('pushAvChatRoomMessageList', JSON.parse(JSON.stringify(message)))
+      this.tim.sendMessage(message).catch(error => {
+        this.$store.commit('showMessage', {
+          type: 'error',
+          message: error.message
+        })
+      })
+      this.messageContent = ''
+    }
+  },
+  watch: {
+    avChatRoomMessageList: function() {
+      this.$nextTick(() => {
+        let messageListNode = this.$refs['message-list']
+        if (!messageListNode) {
+          return
+        }
+        messageListNode.scrollTop = messageListNode.scrollHeight
+      })
+    }
+  }
+}
+</script>
+<style lang="stylus" scoped>
+  ::-webkit-textarea-placeholder {
+    color: #a5b5c1
+  }
+  ::-moz-textarea-placeholder {
+    color: #a5b5c1
+  }
+  ::-ms-textarea-placeholder {
+    color: #a5b5c1
+  }
+  .comment-wrapper {
+    position relative
+    box-sizing border-box
+    width 100%
+    height 100%
+    display flex
+    flex-flow column
+  }
+  .message-list {
+      position: relative;
+      width: 100%;
+      overflow: auto;
+      overflow-x: hidden;
+      -webkit-box-sizing: border-box;
+      box-sizing: border-box;
+      overflow-y: scroll;
+      -webkit-overflow-scrolling: touch;
+      padding: 20px;
+      margin-bottom: 5px;
+      flex 1
+      .msg-item-join {
+        margin: 0 30px 20px 30px
+        padding: 4px 15px
+        border-radius: 3px
+        color: #a5b5c1
+        font-size: 12px
+        text-align: center
+        box-sizing: border-box
+        white-space: nowrap
+        overflow: hidden
+        text-overflow: ellipsis
+
+      }
+      .msg-item-text {
+        width 100%
+        min-height 60px
+        .avatar {
+          width 45px
+          height 45px
+          border-radius 50%
+        }
+        .nick {
+          position relative
+          top -24px
+          left 8px
+          color #a5b5c1
+          font-size 14px
+          margin 0
+          max-width 150px !important
+          display inline-block
+          overflow hidden
+          text-overflow ellipsis
+          white-space nowrap
+        }
+        .msg {
+          position relative
+          top -25px
+          left 60px
+          word-break break-all
+          color #fff
+          margin 0
+          max-width 230px
+          .msg-text {
+            display inline-block
+            height 100%
+            background-color #5cadff
+            border-radius 0 4px 4px 4px
+            padding 10px
+            font-size 14px
+          }
+        }
+        .msg::before {
+          position: absolute
+          top: 0
+          width: 12px
+          height: 40px
+          content: "\E900"
+          font-family: 'tim' !important
+          font-size: 24px
+          transform scaleX(-1)
+          left -10px
+          color #5cadff
+        }
+      }
+      .msg-item-gift {
+        position relative
+        width 60%
+        min-width 290px
+        height 60px
+        background #5cadff
+        border-radius 30px
+        margin 0 0 20px 0
+        .avatar {
+          width 45px
+          height 45px
+          border-radius 50%
+          margin 8px
+        }
+        .nick {
+          position relative
+          top -32px
+          left 0px
+          color #fff
+          margin 0
+          font-size 14px
+          max-width 150px !important
+          display inline-block
+          overflow hidden
+          text-overflow ellipsis
+          white-space nowrap
+        }
+        .msg {
+          position relative
+          top -35px
+          left -5px
+          padding 0 0 0 65px
+          word-break break-all
+          color #fff
+          margin 0
+          font-size 14px
+        }
+        .gift-pic {
+          position absolute
+          top -15px
+          right -18px
+          width 75px
+          height 75px
+        }
+      }
+    }
+    .send-container {
+      position relative
+      width 100%
+      height 100px
+      -webkit-box-sizing border-box
+      box-sizing border-box
+      border-top 1px solid #e6e6e6
+      display flex
+      flex-flow column
+      .comment-message {
+        flex 1
+        margin 0
+        border-radius 0
+        border hidden
+        outline-style none
+        padding 10px
+        font-size 16px
+        color #000
+        resize none
+      }
+      .btn-send {
+        position: absolute
+        color: #2d8cf0
+        font-size: 30px
+        right: 0px
+        bottom: 0px
+        padding: 6px 6px 4px 4px
+        cursor: pointer
+      }
+    }
+</style>

+ 177 - 0
src/components/group-live/components/live-gift.vue

@@ -0,0 +1,177 @@
+<template>
+  <div class="chat-footer-container">
+    <!-- <p class="gift-title">礼物列表</p> -->
+    <carousel :autoplay="false" :loop="false" :initial-index="1" indicator-position="none" arrow="always">
+      <carousel-item v-for="(item, index) in giftList" :key="`item_${index}`">
+        <template v-for="(_item, _index) in item">
+          <div class="gift-item" :key="`gift_item_${index}_${_index}`" @click="handleGiftPic(_item.index)">
+            <img class="gift-icon" :src="_item.icon" alt=""/>
+            <p class="gift-name">{{ _item.name }}</p>
+          </div>
+        </template>
+      </carousel-item>
+    </carousel>
+  </div>
+</template>
+
+<script>
+ import { Carousel , CarouselItem } from 'element-ui'
+  export default {
+    name: 'liveGift',
+    props: {},
+    data() {
+      return {
+        giftList: [
+          [
+            {
+              index: 1,
+              icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590482989_25.png',
+              name: '火箭'
+            },
+            {
+              index: 2,
+              icon: 'https://8.url.cn/huayang/resource/now/new_gift/1507876726_3',
+              name: '鸡蛋'
+            },
+            {
+              index: 3,
+              icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590482294_7.png',
+              name: '吻'
+            },
+          ],
+          [
+            {
+              index: 4,
+              icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590482461_11.png',
+              name: '跑车'
+            },
+            {
+              index: 5,
+              icon: 'https://8.url.cn/huayang/resource/now/new_gift/1594714453_7.png',
+              name: '嘉年华'
+            },
+            {
+              index: 6,
+              icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590482754_17.png',
+              name: '玫瑰'
+            }
+          ],
+          [
+            {
+              index: 7,
+              icon: 'https://8.url.cn/huayang/resource/now/new_gift/1594281297_11.png',
+              name: '直升机'
+            },
+            {
+              index: 8,
+              icon: 'https://8.url.cn/huayang/resource/now/new_gift/1507876472_1',
+              name: '点赞'
+            },
+            {
+              index: 9,
+              icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590483038_27.png',
+              name: '比心'
+            }
+          ],
+          [
+            {
+              index: 10,
+              icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590483168_31.png',
+              name: '冰淇淋'
+            },
+            {
+              index: 11,
+              icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590483225_33.png',
+              name: '玩偶'
+            },
+            {
+              index: 12,
+              icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590483278_35.png',
+              name: '蛋糕'
+            }
+          ],
+          [
+            {
+              index: 13,
+              icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590483348_37.png',
+              name: '豪华轿车'
+            },
+            {
+              index: 14,
+              icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590483429_39.png',
+              name: '游艇'
+            },
+            {
+              index: 15,
+              icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590483505_41.png',
+              name: '翅膀'
+            }
+          ]
+        ]
+      }
+    },
+    components: {
+      Carousel,
+      CarouselItem
+    },
+    methods: {
+      handleGiftPic(index) {
+        this.$bus.$emit('group-live-send-gift', index)
+      }
+    }
+  }
+</script>
+
+<style lang="stylus" scoped>
+ .chat-footer-container {
+   position relative
+   width 100%
+   height 50px
+   box-sizing border-box
+   border-top 1px solid #e6e6e6
+   .gift-title {
+     margin 10px 0 0 0
+     padding 0 10px
+     font-size 16px
+     font-weight 400
+     color #888585
+     border-bottom 1px solid #e6e6e6
+   }
+
+ }
+</style>
+<style>
+  .el-carousel {
+    height: 60px;
+  }
+  .el-carousel .el-carousel__container {
+    height: 100%;
+  }
+  .el-carousel__arrow {
+    top: 40%!important
+  }
+  .el-carousel__item {
+    padding: 0px 30px 0 45px;
+    box-sizing: 'border-box'
+  }
+  .el-carousel__item div {
+    float: left;
+    margin: 0 15px;
+    width: 65px;
+    height: 40px;
+    cursor: pointer;
+  }
+  .el-carousel__item div img {
+    width: 30px;
+    height: 30px;
+    margin: 0 0 0 15px;
+  }
+  .el-carousel__item div p {
+    position: relative;
+    top: -8px;
+    margin: 0;
+    text-align: center;
+    color: #888585;
+    font-size: 12px;
+  }
+</style>

+ 201 - 0
src/components/group-live/components/live-header.vue

@@ -0,0 +1,201 @@
+<template>
+  <div class="header-container">
+    <div v-show="showLiveInfo">
+      <div class="anchor-info">
+        <img class="anchor-avatar" :src="avatar">
+        <div class="anchor-other">
+          <p class="anchor-nick">{{nick}}</p>
+          <p class="online-num">在线:{{onlineMemberCount}}</p>
+        </div>
+      </div>
+      <div class="online-info">
+        <p class="room-name">直播中</p>
+        <img class="living-icon" src="../../../assets/image/living-icon.gif" />
+        <span>{{` ${pusherTime}`}}</span>
+      </div>
+    </div>
+    <div class="close-box" @click="closeLiveMask">
+      <i class="el-icon-circle-close"></i>
+    </div>
+  </div>
+</template>
+
+<script>
+ import { mapState } from 'vuex'
+  export default {
+    name: 'liveHeader',
+    props: {
+      fr: {
+        type: String,
+        requred: true
+      },
+      isPushingStream: {
+        type: Boolean,
+        default: false
+      },
+      stopPushStream: {
+        type: Function
+      },
+      pusherTime: {
+        type: String,
+        default: ''
+      }
+    },
+    data() {
+      return {
+        nick: '',
+        avatar: 'https://imgcache.qq.com/open/qcloud/video/act/webim-avatar/avatar-2.png',
+        onlineMemberCount: 0,
+        timer: null,
+        onlineList: []
+      }
+    },
+    computed: {
+      ...mapState({
+        groupLiveInfo: state => state.groupLive.groupLiveInfo
+      }),
+      showLiveInfo() {
+        return (this.fr === 'pusher' && this.isPushingStream) || this.fr === 'player'
+      },
+      roomName() {
+        return this.groupLiveInfo.roomName || `${this.groupLiveInfo.anchorID}的直播`
+      }
+    },
+    mounted() {
+      this.getAnchorProfile()
+      if (this.fr === 'player') {
+        this.timer = setInterval(() => {
+          this.getGroupOnlineMemberCount()
+        }, 5000)
+      }
+    },
+    beforeDestroy() {
+      this.timer && clearInterval(this.timer)
+    },
+    methods: {
+      closeLiveMask() {
+        if (this.fr === 'pusher') {
+          this.stopPushStream()
+          return
+        }
+        this.$store.commit('updateGroupLiveInfo', { isNeededQuitRoom: 1 })
+        this.$bus.$emit('close-group-live')
+      },
+      async getAnchorProfile() {
+        const res = await this.tim.getUserProfile({userIDList: [this.groupLiveInfo.anchorID]})
+        if (res.code === 0) {
+          this.nick = res.data[0].nick || res.data[0].userID
+          this.avatar = res.data[0].avatar || 'https://imgcache.qq.com/open/qcloud/video/act/webim-avatar/avatar-2.png'
+        }
+      },
+      async getGroupOnlineMemberCount() {
+        const res = await this.tim.getGroupOnlineMemberCount(this.groupLiveInfo.roomID)
+        if (res.code === 0 && res.data) {
+          this.onlineMemberCount = res.data.memberCount
+        }
+      }
+    },
+    watch: {
+      isPushingStream: function(val) {
+        if (val && this.fr === 'pusher') {
+          this.timer = setInterval(() => {
+            this.getGroupOnlineMemberCount()
+          }, 5000)
+        }
+      }
+    }
+  }
+</script>
+
+<style lang="stylus" scoped>
+ .header-container {
+   position absolute
+   left 0
+   top 0
+   width 100%
+   height 100%
+   box-sizing border-box
+   z-index 99
+   padding 10px 10px 10px 20px
+  .anchor-info {
+    position absolute
+    top 50%
+    transform translateY(-50%)
+    width 200px
+    height 50px
+    background rgba(255, 255 ,255 ,0.1)
+    border-radius 30px
+    display flex
+    align-items center
+    .anchor-avatar {
+      width 50px
+      height 50px
+      border-radius 50%
+      margin 0 5px
+    }
+    .anchor-other {
+      height 100%
+      flex 1
+      p {
+        margin 0
+      }
+      .anchor-nick{
+        max-width 140px
+        margin 6px 0 0 0
+        color: #ffffff
+        font-weight 500
+        word-break keep-all
+        overflow hidden
+        text-overflow ellipsis
+        white-space nowrap
+      }
+      .online-num{
+        font-size 14px
+        font-weight 400
+        color #d2cbcbad
+      }
+    }
+  }
+  .online-info {
+    position absolute
+    left 50%
+    top 50%
+    transform translate(-50%, -50%)
+    height 50px
+    color #fff
+    display flex
+    align-items center
+    .room-name{
+      display inline-block
+      max-width 160px
+      overflow hidden
+      white-space nowrap
+      text-overflow ellipsis
+      margin 0
+      padding 0 0 0 10px
+    }
+    .living-icon{
+      position relative
+      top -3px
+      margin 0 5px
+      width 25px
+    }
+    span {
+      margin 2px 0 0 0
+    }
+  }
+  .close-box {
+    position absolute
+    right 0
+    top 0px
+    width 70px
+    height 70px
+    color: #959798
+    font-size 36px
+    cursor pointer
+    display flex
+    align-items center
+    justify-content center
+  }
+ }
+</style>

+ 331 - 0
src/components/group-live/components/live-player.vue

@@ -0,0 +1,331 @@
+<template>
+  <div class="player">
+    <div class="header-bar">
+      <live-header  fr="player" />
+    </div>
+    <div id="player-container" class="video-container">
+      <div style="position: absolute">
+        <img src="../../../assets/image/no-video.png">
+        <p style="font-size: 24px;color: #8A9099;text-align: center;margin-top: 30px">暂无画面</p>
+      </div>
+    </div>
+    <div class="setting-bar">
+      <!-- <live-share /> -->
+      <div>
+        <div class="player-start cursor" v-if="!isPlaying" @click="resumeVideo">
+          <img class="player-icon" src="../../../assets/image/web-pusher-start.png">
+          <span class="play-text">观看直播</span>
+        </div>
+        <div class="player-start cursor" v-else @click="pauseVideo">
+          <img class="player-icon" src="../../../assets/image/pause-icon.png">
+          <span class="play-text">暂停观看</span>
+        </div>
+      </div>
+      <div class="volume-box" ref="volumeBox">
+        <p class="setting-icon cursor " @click="setPlayoutVolume">
+          <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <mask id="path-1-inside-1" fill="white">
+              <path fill-rule="evenodd" clip-rule="evenodd" d="M4.18171 12H0V4H4.19181L8 0.831913L9 0V1.3008V14.7003V16L8 15.1698L4.18171 12ZM5 4.62845L8 2.13271V13.8701L5 11.3796V4.62845Z"/>
+            </mask>
+            <path d="M0 12V14H-2V12H0ZM4.18171 12V10H4.9037L5.45921 10.4612L4.18171 12ZM0 4H-2V2H0V4ZM4.19181 4L5.47089 5.53752L4.91496 6H4.19181V4ZM8 0.831913L6.72092 -0.705603V-0.705603L8 0.831913ZM9 0L7.72092 -1.53752L11 -4.26543V0H9ZM9 16H11V20.2597L7.72251 17.5388L9 16ZM8 15.1698L9.27749 13.631H9.27749L8 15.1698ZM5 4.62845H3V3.69068L3.72092 3.09094L5 4.62845ZM8 2.13271L6.72092 0.595197L10 -2.13271V2.13271H8ZM8 13.8701H10V18.1299L6.72251 15.409L8 13.8701ZM5 11.3796L3.72251 12.9185L3 12.3187V11.3796H5ZM0 10H4.18171V14H0V10ZM2 4V12H-2V4H2ZM4.19181 6H0V2H4.19181V6ZM2.91273 2.46248L6.72092 -0.705603L9.27908 2.36943L5.47089 5.53752L2.91273 2.46248ZM6.72092 -0.705603L7.72092 -1.53752L10.2791 1.53752L9.27908 2.36943L6.72092 -0.705603ZM11 0V1.3008H7V0H11ZM11 1.3008V14.7003H7V1.3008H11ZM11 14.7003V16H7V14.7003H11ZM7.72251 17.5388L6.72251 16.7087L9.27749 13.631L10.2775 14.4612L7.72251 17.5388ZM6.72251 16.7087L2.90422 13.5388L5.45921 10.4612L9.27749 13.631L6.72251 16.7087ZM3.72092 3.09094L6.72092 0.595197L9.27908 3.67023L6.27908 6.16597L3.72092 3.09094ZM10 2.13271V13.8701H6V2.13271H10ZM6.72251 15.409L3.72251 12.9185L6.27749 9.8408L9.27749 12.3313L6.72251 15.409ZM3 11.3796V4.62845H7V11.3796H3Z" fill="#8A9099" mask="url(#path-1-inside-1)"/>
+            <path fill-rule="evenodd" clip-rule="evenodd" d="M11.6088 13.9991C13.9991 13.0388 15.6868 10.6992 15.6868 7.9654C15.6868 5.24314 14.0133 2.91174 11.639 1.94385L10.9332 3.81684C12.5511 4.49874 13.6868 6.09944 13.6868 7.9654C13.6868 9.84413 12.5355 11.454 10.8999 12.1278L11.6088 13.9991Z" fill="#8A9099"/>
+          </svg>
+          <span class="mic-text">声音</span>
+        </p>
+        <span class="progress-box" ref="progressBox" v-show="showSetVolume">
+          <el-slider vertical height="200px" @change="setPlayoutVolume" v-model="volumeValue"></el-slider>
+        </span>
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+import { mapState } from 'vuex'
+import { Slider } from 'element-ui'
+import liveHeader from './live-header'
+// import liveShare from './live-share'
+import poster from '../../../assets/image/poster.png'
+
+export default {
+  name: 'livePlayer',
+  data() {
+    return {
+      player: null,
+      isPlaying: true, // 是否正在播放
+      volumeValue: 70,
+      showSetVolume: false,
+      isMute: false,
+    }
+  },
+  computed: {
+    ...mapState({
+      user: state => state.imuser,
+      roomID: state => state.groupLive.groupLiveInfo.roomID,
+      anchorID: state => state.groupLive.groupLiveInfo.anchorID,
+    }),
+  },
+  mounted() {
+    this.init()
+    const volumeBox = this.$refs.volumeBox
+    const progressBox = this.$refs.progressBox
+    progressBox.addEventListener('mouseover', () => {
+      this.showSetVolume = true
+    })
+    progressBox.addEventListener('mouseout', () => {
+      this.showSetVolume = false
+    })
+    volumeBox.addEventListener('mouseover', () => {
+      this.showSetVolume = true
+    })
+    volumeBox.addEventListener('mouseout', () => {
+      this.showSetVolume = false
+    })
+  },
+  beforeDestroy() {
+    this.stopPlay()
+  },
+  components: {
+    liveHeader,
+    // liveShare,
+    elSlider: Slider
+  },
+  methods: {
+    // 初始化
+    init() {
+      this.player = this.TWebLive.createPlayer()
+      this.player.setCustomConfig({
+        autoplay: true,
+        poster: { style:'cover', src: poster },
+        pausePosterEnabled: false,
+        wording: {
+          1:'您观看的直播已结束哦~ ',
+          2:'您观看的直播已结束哦~ ',
+          4:'您观看的直播已结束哦~ ',
+          13:'您观看的直播已结束',
+          2032: '请求视频失败,请检查网络',
+          2048: '请求m3u8文件失败,可能是网络错误或者跨域问题'
+        }
+      })
+      // 播放时
+      this.player.on(this.TWebLive.EVENT.PLAYER_PLAYING, this.onPlayerPlaying)
+      // 暂停
+      this.player.on(this.TWebLive.EVENT.PLAYER_PAUSE, this.onPlayerPause)
+      // 浏览器不允许自动播放
+      this.player.on(this.TWebLive.EVENT.PLAYER_AUTOPLAY_NOT_ALLOWED, this.onPlayerAutoPlayNotAllowed)
+      this.player.on(this.TWebLive.EVENT.PLAYER_ERROR, this.onPlayerError)
+      this.setRenderView()
+      this.$bus.$emit('join-group-live-avchatroom')
+    },
+    // eslint-disable-next-line no-unused-vars
+    onPlayerPlaying(event) {},
+    // eslint-disable-next-line no-unused-vars
+    onPlayerPause(event) {},
+    // eslint-disable-next-line no-unused-vars
+    onPlayerAutoPlayNotAllowed(event) {
+      this.$store.commit('showMessage', {
+        message: '不能自动播放',
+        type: 'info'
+      })
+    },
+    // eslint-disable-next-line no-unused-vars
+    onPlayerError(event) {},
+    // 设置渲染界面
+    setRenderView() {
+      this.player.setRenderView({ elementID: 'player-container' })
+      this.startPlay()
+    },
+    //开始播放
+    startPlay() {
+      const streamID = `${this.user.sdkAppID}_${this.roomID}_${this.anchorID}_main`
+      const flv = `https://tuikit.qcloud.com/live/${streamID}.flv`
+      const hls = `https://tuikit.qcloud.com/live/${streamID}.m3u8` 
+      const url = `https://flv=${encodeURIComponent(flv)}&hls=${encodeURIComponent(hls)}`
+      this.player.startPlay(url).then(() => {
+        this.isPlaying = true
+      }).catch(() => {})
+    },
+    resumeAudio() {
+      this.player.resumeAudio().then(() => {
+        this.isMute = false
+      }).catch(() => {})
+    },
+    pauseAudio() {
+      this.player.pauseAudio().then(() => {
+        this.isMute = true
+      }).catch(() => {})
+    },
+    //暂停播放
+    pauseVideo() {
+      this.player.pauseVideo().then(() => {
+        this.isPlaying = false
+      }).catch(() => {})
+    },
+    // 恢复播放
+    resumeVideo() {
+      this.player.resumeVideo().then(() => {
+        this.isPlaying = true
+      }).catch(() => {})
+    },
+    setPlayoutVolume() {
+      this.player.setPlayoutVolume(this.volumeValue)
+    },
+    // 停止播放
+    stopPlay() {
+      this.player.stopPlay()
+      this.isPlaying = false
+    },
+  }
+}
+</script>
+<style  lang="stylus">
+.vcp-controls-panel {
+  display none
+}
+.el-slider__bar {
+  background #5cadff !important
+}
+.el-slider__button {
+  border hidden
+}
+</style>
+<style lang="stylus" scoped>
+  .cursor {
+    cursor: pointer;
+  }
+  .active {
+    color: #2d8cf0
+  }
+  .setting-icon {
+    display flex
+    justify-content center
+    align-items center
+    height 40px
+    margin 0 10px
+  }
+  .player {
+    position relative
+    width 99.9%
+    height 100%
+    display flex
+    flex-flow column
+    .video-container {
+      position relative
+      display flex
+      flex-direction column
+      justify-content center
+      align-items center
+      flex 1
+      background rgba(0, 0, 0, 0.3)
+      .stop-camera {
+        position absolute
+        top 0
+        right 0
+        bottom 0
+        left 0
+        background-color #ffffff
+        display flex
+        flex-direction column
+        justify-content center
+        align-items center
+      }
+    }
+    .header-bar {
+      position: relative;
+      width 100%
+      height 70px
+      background-color #363e47
+      padding 10px 10px 10px 20px
+    }
+    .setting-bar {
+      position: relative;
+      bottom 0
+      width 100%
+      height 55px
+      background-color #363e47
+      .player-time{
+        position: absolute;
+        bottom 0
+        left 15px
+        width 100%
+        line-height 60px
+        font-size: 14px;
+        color: #D2DAE6;
+        letter-spacing: 0;
+      }
+      .player-start {
+        position: absolute;
+        right 0
+        bottom 0
+        width 210px
+        height 55px
+        background #5cadff
+        display: flex;
+        justify-content: center;
+        align-items: center;
+      }
+      .player-mic{
+        position: absolute;
+        right 260px
+        bottom 0
+        /*width 210px*/
+        height 55px
+        display: flex;
+        justify-content: center;
+        align-items: center
+        padding 0 10px
+      }
+      .play-text{
+        font-size 16px
+        color #ffffff
+        margin-left 5px
+      }
+      .player-icon{
+        width 14px
+        height 14px
+      }
+      .mic-text{
+        font-size 14px
+        color #8A9099
+        margin-left 5px
+      }
+    }
+    .volume-box {
+      position absolute
+      right 220px
+      bottom 8px
+      .progress-box {
+        padding 4px 0
+        position absolute
+        bottom 35px
+        right 32px
+      }
+    }
+    /deep/ .vcp-player video {
+      position absolute
+      top 0
+      left 0
+      width 100% !important
+      height 100% !important
+      object-fit cover
+    }
+
+    /deep/ .vcp-player {
+      width 100% !important
+      height 100% !important
+      position: relative;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      background-color:rgba(0,0,0,0)
+    }
+
+    /deep/ .vcp-bigplay {
+      display none
+    }
+
+    /deep/ .vcp-error-tips {
+      color #FFFFFF
+      margin-top: -8.25em
+    }
+  }
+</style>

+ 378 - 0
src/components/group-live/components/live-pusher.vue

@@ -0,0 +1,378 @@
+<template>
+  <div class="pusher">
+    <div class="header-bar">
+      <live-header fr="pusher" :pusherTime="pusherTime" :isPushingStream="isPushingStream" :stopPushStream="stopPushStream"/>
+      <div class="input-name-box" v-show="!isPushingStream">
+        <img class="avatar" :src="anchorAvatar" alt="" />
+        <input class="room-name" v-model="roomName" placeholder="标题有趣吸引人气" />
+      </div>
+    </div>
+    <div id="video-container" class="video-container">
+    </div>
+    <div class="setting-bar">
+      <!-- <live-share v-if="isPushingStream" /> -->
+      <div>
+        <div v-if="!isPushingStream"  class="pusher-start cursor" @click="startPushStream">
+          <img class="pusher-icon" src="../../../assets/image/web-pusher-start.png">
+          <span class="play-text">开始直播</span>
+        </div>
+        <div v-else class="pusher-start cursor" @click="stopPushStream">
+          <img class="pusher-icon" src="../../../assets/image/web-pusher-stop.png">
+          <span class="play-text">结束直播</span>
+        </div>
+      </div>
+      <div>
+        <div v-if="isMute" class="pusher-mic cursor" @click="startMicrophone">
+          <img class="pusher-icon" src="../../../assets/image/close-mic.png">
+          <span class="mic-text">麦克风</span>
+        </div>
+        <div v-else class="pusher-mic cursor" @click="stopMicrophone">
+          <img class="pusher-icon" src="../../../assets/image/open-mic.png">
+          <span class="mic-text">麦克风</span>
+        </div>
+      </div>
+      <div>
+        <div v-if="isStartCamera" class="pusher-mic cursor" style="right: 300px" @click="startCamera">
+          <svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+            <title>摄像头关闭</title>
+            <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+              <g id="j进度条备份" transform="translate(-751.000000, -22.000000)">
+                <g id="编组-9" transform="translate(751.000000, 22.000000)">
+                  <g id="编组-11">
+                    <path d="M15,7.5 C15,11.6421356 11.6421356,15 7.5,15 C6.57050116,15 5.68049488,14.8309122 4.85906245,14.5218179 L6.43561423,12.8970864 C6.77997642,12.9646116 7.13585551,13 7.5,13 C10.5375661,13 13,10.5375661 13,7.5 C13,7.0789865 12.9526952,6.66902162 12.8631108,6.27513055 L14.4397563,4.65055194 C14.8008626,5.52907797 15,6.49128347 15,7.5 Z M7.5,0 C9.88955323,0 12.0181001,1.11750106 13.3915381,2.85840064 L11.9813778,4.31063973 C10.984196,2.91201268 9.34860401,2 7.5,2 C4.46243388,2 2,4.46243388 2,7.5 C2,9.40763139 2.97118511,11.0884303 4.44609208,12.0749336 L3.03637255,13.5276986 C1.1940301,12.1611583 0,9.97001947 0,7.5 C0,3.35786438 3.35786438,0 7.5,0 Z" id="形状结合" fill="#8A9099" fill-rule="nonzero"></path>
+                    <path d="M7.5,5 C8.54350703,5 9.43769004,5.63933214 9.81221332,6.5476607 L6.61637488,9.83935679 C5.67174144,9.48236445 5,8.56962904 5,7.5 C5,6.11928813 6.11928813,5 7.5,5 Z" id="形状结合" fill="#8A9099"></path>
+                    <path d="" id="形状结合" stroke="#8A9099" stroke-width="2"></path>
+                    <line x1="13.3137085" y1="2" x2="2" y2="13.3137085" id="直线-5" stroke="#8A9099" stroke-width="2" stroke-linecap="square"></line>
+                  </g>
+                </g>
+              </g>
+            </g>
+          </svg>
+          <span class="mic-text">摄像头</span>
+        </div>
+        <div v-else class="pusher-mic cursor" style="right: 300px" @click="stopCamera">
+          <img class="pusher-icon" src="../../../assets/image/camera-open.png">
+          <span class="mic-text">摄像头</span>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import axios from 'axios'
+import { mapState } from 'vuex'
+import liveHeader from './live-header'
+// import liveShare from './live-share'
+import { formatTime } from '../../../utils/date.js'
+
+export default {
+  name: 'livePusher',
+  data() {
+    return {
+      pusher: null,
+      roomID: 0,
+      roomName: '',
+      isPushingStream: false, // 是否正在推流
+      updateTimer: 0,
+      pusherTime: '00:00:00',
+      time: 0, // 直播时长 秒
+      recordTimer: null, // 记录直播时长
+      isMute: false,   //是否禁言
+      isStartCamera: true
+    }
+  },
+  computed: {
+    ...mapState({
+      user: state => state.imuser,
+      groupLiveInfo: state => state.groupLive.groupLiveInfo
+    }),
+    anchorAvatar() {
+      return this.user.currentUserProfile.avatar || 'https://imgcache.qq.com/open/qcloud/video/act/webim-avatar/avatar-2.png'
+    }
+  },
+  created() {
+    this.$store.commit('resetGroupLiveInfo', { roomID: 0 })
+  },
+  mounted() {
+    this.init()
+  },
+  async beforeDestroy() {
+    if (this.isPushingStream) {
+      clearInterval(this.updateTimer)
+      clearInterval(this.recordTimer)
+      await this.stopPush()
+    }
+  },
+  components: {
+    liveHeader,
+    // liveShare,
+  },
+  methods: {
+    // 初始化
+    init() {
+      this.pusher = this.TWebLive.createPusher({
+        userID: this.user.userID
+      })
+      this.setRenderView()
+      this.pusher.on(this.TWebLive.EVENT.RTC_CONNECTION_STATE_CHANGED, this.onRTCConnectionStateChanged)
+      this.pusher.on(this.TWebLive.EVENT.RTC_CLIENT_BANNED, this.onRTCClientBanned)
+      this.pusher.on(this.TWebLive.EVENT.RTC_CLIENT_ERROR, this.onRTCError)
+    },
+    // eslint-disable-next-line no-unused-vars
+    onRTCConnectionStateChanged(event) {},
+    // eslint-disable-next-line no-unused-vars
+    onRTCClientBanned(event) {},
+    // eslint-disable-next-line no-unused-vars
+    onRTCError(event) {},
+    //开启本地预览
+    setRenderView() {
+      this.pusher.setRenderView({
+        elementID: 'video-container',
+        audio: true,
+        video: true
+      }).then(() => {
+        // 设置背景
+       let el = window.document.getElementById('video-container').childNodes
+        el[0].style.backgroundColor = 'rgba(0,0,0,0)'
+        this.isStartCamera = false
+      }).catch(() => {})
+    },
+    // 摄像头、麦克风操作
+    startCamera() {
+      this.pusher.startCamera().then(() => {
+        this.isStartCamera = false
+      }).catch(() => {})
+    },
+    stopCamera() {
+      this.pusher.stopCamera().then(() => {
+        this.isStartCamera = true
+      }).catch(() => {})
+    },
+    startMicrophone() {
+      this.pusher.startMicrophone().then(() => {
+        this.isMute = false
+      }).catch(() => {})
+    },
+    stopMicrophone() {
+      this.pusher.stopMicrophone().then(() => {
+        this.isMute = true
+      }).catch(() => {})
+    },
+    // 生成roomID
+    generateRoomID(min, max) {
+      return Math.floor(Math.random()*(max - min) + min).toString()
+    },
+    // 创建直播房间
+    async createRoom() {
+      this.roomID = this.generateRoomID(1000, 2000000000)
+      this.roomName = this.roomName ? this.roomName : `${this.user.userID}的直播`
+      await axios (`https://service-62h5r0ea-1252463788.gz.apigw.tencentcs.com/release/forTestAdvanced?method=createRoom&appId=${this.user.sdkAppID}&type=groupLive&title=${this.roomName}&anchorId=${this.user.userID}&roomId=${this.roomID}`)
+      this.$store.commit('updateGroupLiveInfo', { roomID: this.roomID, roomName: this.roomName })
+      this.createGroupLiveAvChatRoom()
+    },
+    // 解散直播间
+    async destroyRoom() {
+      await axios (`https://service-c2zjvuxa-1252463788.gz.apigw.tencentcs.com/release/forTest?method=destroyRoom&appId=${this.user.sdkAppID}&type=groupLive&roomId=${this.roomID}`)
+    },
+    // 更新直播间 10s 上报一次,心跳保活,如果不上报,后台检测不到心跳会销毁房间
+    updateRoom() {
+      axios (`https://service-c2zjvuxa-1252463788.gz.apigw.tencentcs.com/release/forTest?method=updateRoom&appId=${this.user.sdkAppID}&type=groupLive&roomId=${this.roomID}`)
+    },
+    // 创建直播互动群
+    async createGroupLiveAvChatRoom() {
+      await this.tim.createGroup({
+        name: this.roomName,
+        groupID: this.roomID,
+        type: this.OpenIM.TYPES.GRP_AVCHATROOM,
+      })
+      this.$bus.$emit('join-group-live-avchatroom')
+    },
+    //开始推流
+    async startPushStream() {
+      await this.createRoom()
+      //streamID 拼接规则: sdkappid_roomid_userid_main
+      const streamID = `${this.user.sdkAppID}_${this.roomID}_${this.user.userID}_main`
+      // 对userSig进行encode,防止userSig中带有+时被浏览器解析为空格,导致trtc ws连接失败
+      const url = `room://livedomainname=tuikit.qcloud.com&sdkappid=${this.user.sdkAppID}&roomid=${this.roomID}&userid=${this.user.userID}&usersig=${encodeURIComponent(this.user.userSig)}&streamid=${streamID}`
+      this.pusher.startPush(url).then(() => {
+        this.isPushingStream = true
+        this.sendNoticeToGroup(1)
+        this.updateTimer = setInterval(() => {
+          this.updateRoom()
+        }, 10000)
+        this.recordTimer = setInterval(() => {
+          this.recordLiveTime()
+        }, 1000)
+      }).catch(() => {})
+    },
+    // 停止推流
+    stopPushStream() {
+      // 派发关闭浮层组件事件
+      this.$bus.$emit('close-group-live')
+    },
+    async stopPush() {
+      await this.destroyRoom()
+      await this.pusher.stopPush()
+      await this.tim.dismissGroup(this.roomID) // 解散直播群组
+      this.isPushingStream = false
+      this.sendNoticeToGroup(0)
+    },
+    // 给群内发送开始直播、结束直播自定义消息
+    // roomStatus 1 开始直播 0 结束直播
+    sendNoticeToGroup(roomStatus) {
+      if (!this.groupLiveInfo.groupID) {
+        return
+      }
+      const { userID, nick, avatar } = this.user.currentUserProfile
+      const form = {
+        roomId: this.roomID,
+        roomName: this.roomName,
+        roomCover: avatar,
+        roomStatus: `${roomStatus}`,
+        anchorName: nick,
+        version: 4,
+        roomType: 'liveRoom',
+        anchorId: userID,
+        businessID: 'group_live'
+      }
+      const message = this.tim.createCustomMessage({
+        to: this.groupLiveInfo.groupID,
+        conversationType: 3,
+        priority: this.OpenIM.TYPES.MSG_PRIORITY_NORMAL,
+        payload: {
+          data: JSON.stringify(form),
+          description: '',
+          extension: '',
+        },
+      })
+      this.$store.commit('pushCurrentMessageList', message)
+      this.tim.sendMessage(message).then(() => {}).catch(() => {})
+    },
+    // 记录直播时间
+    recordLiveTime () {
+      this.time++
+      this.pusherTime = formatTime(this.time)
+    }
+  }
+}
+</script>
+
+<style lang="stylus"  scoped>
+  ::-webkit-input-placeholder {
+    color: #fff
+  }
+  ::-moz-input-placeholder {
+    color: #fff
+  }
+  ::-ms-input-placeholder {
+    color: #fff
+  }
+  .cursor {
+    cursor: pointer;
+  }
+  .active {
+    color: #2d8cf0
+  }
+  .pusher {
+    position relative
+    width 100%
+    height 100%
+    background rgba(0, 0, 0, 1)
+    display flex
+    flex-flow column
+    flex-direction column
+    .video-container {
+      position relative
+      height calc(100% - 125px)
+      .stop-camera {
+        position absolute
+        top 0
+        right 0
+        bottom 0
+        left 0
+        background-color #ffffff
+        display flex
+        flex-direction column
+        justify-content center
+        align-items center
+      }
+    }
+    .header-bar {
+      position: relative;
+      width 100%
+      height 70px
+      background-color #363e47
+      padding 10px 10px 10px 20px
+      .input-name-box{
+        position absolute
+        left 0
+        top 0px
+        width calc(100% - 100px)
+        height 100%
+        display flex
+        align-items center
+        z-index 99
+        padding 10px 20px
+        .avatar{
+          width 50px
+          height 50px
+          border-radius 50%
+          margin 0px 10px 0px 0px
+        }
+        .room-name{
+          border hidden
+          outline-style none
+          height 40px
+          width  60%
+          font-size 20px
+          color #fff
+          background rgba(255, 255,255, 0)
+          border-bottom 1px solid #fff
+        }
+      }
+    }
+    .setting-bar {
+      position: relative;
+      width 100%
+      height 55px
+      background-color #363e47
+      .pusher-start {
+        position: absolute;
+        right 0
+        bottom 0
+        width 210px
+        height 55px
+        background #5cadff
+        display: flex;
+        justify-content: center;
+        align-items: center;
+      }
+      .pusher-mic{
+        position: absolute;
+        right 220px
+        bottom 0
+        /*width 210px*/
+        height 55px
+        display: flex;
+        justify-content: center;
+        align-items: center
+        padding 0 10px
+      }
+      .play-text{
+        font-size 16px
+        color #ffffff
+        margin-left 5px
+      }
+      .pusher-icon{
+        width 14px
+        height 14px
+      }
+      .mic-text{
+        font-size 14px
+        color #8A9099
+        margin-left 5px
+      }
+    }
+  }
+</style>

+ 113 - 0
src/components/group-live/components/live-share.vue

@@ -0,0 +1,113 @@
+<template>
+  <div>
+    <div class="share-content"  ref="shareCon" v-show="showShareContent">
+      <p class="qrcode-tips">手机扫码观看或复制链接分享给好友</p>
+      <qrcode ref="childQrcode"/>
+      <button class="copy-link" @click="copyLink" v-clipboard="playUrl" v-clipboard:success="onCopySuccess" v-clipboard:error="onCopyError">复制链接</button>
+    </div>
+    <div class="share-btn" ref="shareBtn">
+      <img class="share-icon" src="../../../assets/image/share-icon.png" alt=""/>
+      分享直播
+    </div>
+  </div>
+</template>
+
+<script>
+import qrcode from './qrcode'
+export default {
+  name: 'liveShare',
+  data() {
+    return {
+      showShareContent: false,
+      playUrl: '',
+    }
+  },
+  computed: {},
+  components: {
+    qrcode
+  },
+  mounted() {
+    const shareCon = this.$refs.shareCon
+    const shareBtn = this.$refs.shareBtn
+    shareBtn.addEventListener('mouseover', () => {
+      this.showShareContent = true
+    })
+    shareBtn.addEventListener('mouseout', () => {
+      this.showShareContent = false
+    })
+    shareCon.addEventListener('mouseover', () => {
+      this.showShareContent = true
+    })
+    shareCon.addEventListener('mouseout', () => {
+      this.showShareContent = false
+    })
+  },
+  methods: {
+    copyLink() {
+      this.playUrl= this.$refs.childQrcode.playUrl
+    },
+    onCopySuccess() {
+      this.$store.commit('showMessage', {
+        type: 'success',
+        message: '复制成功'
+      })
+    },
+    onCopyError() {
+      this.$store.commit('showMessage', {
+        type: 'error',
+        message: '复制失败'
+      })
+    }
+  }
+}
+</script>
+<style lang="stylus" scoped>
+  .share-content {
+    position absolute
+    top -250px
+    left 20px
+    width 200px
+    height 250px
+    background #ffffff
+    border-radius 5px 5px 0 0
+    z-index 1
+    padding 10px
+    box-sizing border-box
+    text-align center
+    .qrcode-tips {
+      margin 0 0 0 0
+      color #a5b5c1
+      text-align center
+    }
+    .copy-link{
+      width 160px
+      height 40px
+      border hidden
+      outline-style none
+      background #5cadff
+      color #fff
+      font-size 16px
+      border-radius 25px
+      margin 20px 0
+      cursor pointer
+    }
+  }
+  .share-btn {
+    position absolute
+    bottom 0
+    line-height 55px
+    font-size 14px
+    color #8a9099
+    letter-spacing 0
+    margin 0 0 0 20px
+    box-sizing border-box
+    display flex
+    align-items center
+    cursor pointer
+    .share-icon {
+      width 20px
+      height 20px
+      margin 0 5px 2px 0
+    }
+  }
+</style>

+ 47 - 0
src/components/group-live/components/qrcode.vue

@@ -0,0 +1,47 @@
+<template>
+  <img class="qrcode-img" v-if="qrcodeUrl" :src="qrcodeUrl"/>
+</template>
+
+<script>
+import { mapState } from 'vuex'
+import QRCode from 'qrcode'
+export default {
+  props: {
+    url: String
+  },
+  data() {
+    return {
+      qrcodeUrl: '',
+      playUrl: '',
+    }
+  },
+  computed: {
+    ...mapState({
+      user: state => state.imuser,
+      roomID: state => state.groupLive.groupLiveInfo.roomID,
+      anchorID: state => state.groupLive.groupLiveInfo.anchorID,
+    }),
+  },
+  async mounted() {
+    this.qrcodeUrl = await this.generateQRcode()
+  },
+  methods: {
+    generateQRcode() {
+      // 群直播暂时没有分享二维码功能,以下代码注释,避免lint error
+      // const streamID = `${this.user.sdkAppID}_${this.roomID}_${this.anchorID}_main`
+      // const flv = `https://tuikit.qcloud.com/live/${streamID}.flv`
+      // const hls = `https://tuikit.qcloud.com/live/${streamID}.m3u8` 
+      // this.playUrl = ''
+      return QRCode.toDataURL('')
+    }
+  }
+}
+</script>
+<style lang="stylus" scoped>
+  .qrcode-img {
+    display block
+    width 120px
+    height  120px
+    margin 0 auto
+  }
+</style>

+ 90 - 0
src/components/group-live/index.vue

@@ -0,0 +1,90 @@
+<template>
+  <div class="group-live-mask" v-if="groupLiveVisible">
+        <div class="live-container">
+            <div class="video-wrap">
+              <template v-if="channel === 3 && userID !== anchorID">
+                <live-player />
+              </template>
+              <template v-else>
+                <live-pusher />
+              </template>
+            </div>
+            <div class="chat-wrap">
+              <live-chat v-if="groupLiveVisible" />
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+import { mapState } from 'vuex'
+import livePusher from './components/live-pusher'
+import livePlayer from './components/live-player'
+import liveChat from './components/live-chat'
+
+export default {
+  name: 'groupLive',
+  data() {
+    return {
+      groupLiveVisible: false,
+      channel: 1 // 进入直播间渠道:1 群组内直播 2 群组外直播 3 点击消息卡片
+    }
+  },
+  computed: {
+    ...mapState({
+      userID: state => state.imuser.userID,
+      groupID: state => state.groupLive.groupLiveInfo.groupID,
+      roomID: state => state.groupLive.groupLiveInfo.roomID,
+      anchorID: state => state.groupLive.groupLiveInfo.anchorID,
+    }),
+  },
+  mounted() {
+    this.$bus.$on('open-group-live', (options) => {
+      this.channel = options.channel
+      this.groupLiveVisible = true
+    })
+    this.$bus.$on('close-group-live', () => {
+      this.groupLiveVisible = false
+      this.$store.commit('clearAvChatRoomMessageList')
+    })
+  },
+  beforeDestroy() {
+    this.$bus.$off('open-group-live')
+    this.$bus.$off('close-group-live')
+  },
+  components: {
+      livePusher,
+      livePlayer,
+      liveChat,
+  },
+  methods: {}
+}
+</script>
+<style lang="stylus" scoped>
+    .group-live-mask{
+      position absolute
+      top 8vh
+      width 80vw
+      height 80vh
+      max-width: 1280px
+      background: #fff
+      z-index 999
+    }
+    .live-container {
+        width 100%
+        height 100%
+        display flex
+        .video-wrap {
+          position relative
+          flex 1
+          min-width 500px
+          height 100%
+          background url('../../assets/image/video-bg.png') center no-repeat
+        }
+        .chat-wrap {
+          width 375px
+          height 100%
+          background #f5f5f5
+        }
+    }
+</style>

+ 234 - 0
src/components/group/create-group.vue

@@ -0,0 +1,234 @@
+<template>
+  <div>
+    <el-form :model="form" :rules="rules" ref="createGroupForm" label-width="100px">
+      <!--      <el-form-item label="群ID">-->
+      <!--        <el-input v-model="form.groupID"></el-input>-->
+      <!--      </el-form-item>-->
+      <el-form-item label="群名称" prop="groupInfo.groupName">
+        <el-input v-model="form.groupInfo.groupName"></el-input>
+      </el-form-item>
+      <el-form-item label="群头像" prop="groupInfo.faceURL">
+        <el-upload
+          v-model="form.groupInfo.faceURL"
+          class="avatar-uploader"
+          :action="avataruploadUrl"
+          :show-file-list="false"
+          :on-success="avatarhandleAvatarSuccess"
+          :before-upload="beforeAvatarUpload">
+          <img v-if="form.groupInfo.faceURL" :src="form.groupInfo.faceURL" class="avatar" width="150px" >
+          <i v-else class="el-icon-plus avatar-uploader-icon"></i>
+        </el-upload>
+      </el-form-item>
+      <el-form-item label="群简介">
+        <el-input type="textarea" v-model="form.groupInfo.introduction" :maxlength="240"></el-input>
+      </el-form-item>
+      <el-form-item label="群公告">
+        <el-input type="textarea" v-model="form.groupInfo.notification" :maxlength="300"></el-input>
+      </el-form-item>
+      <!--      <el-form-item label="加群方式">-->
+      <!--        <el-radio-group v-model="form.joinOption" :disabled="joinOptionDisabled">-->
+      <!--          <el-radio label="FreeAccess">自由加群</el-radio>-->
+      <!--          <el-radio label="NeedPermission">需要验证</el-radio>-->
+      <!--          <el-radio label="DisableApply">禁止加群</el-radio>-->
+      <!--        </el-radio-group>-->
+      <!--      </el-form-item>-->
+      <el-form-item label="群成员列表">
+        <el-select
+          v-model="form.memberUserIDs"
+          multiple
+          filterable
+          placeholder="请选择群成员"
+          :loading="loading"
+        >
+          <el-option
+            v-for="item in options"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          >
+            <span>{{ item.label }}</span>
+            <span style="float: right; color: #999;">{{ item.value }}</span>
+          </el-option>
+        </el-select>
+      </el-form-item>
+
+    </el-form>
+    <div slot="footer">
+      <el-button type="primary" @click="onSubmit('createGroupForm')">立即创建</el-button>
+      <el-button @click="closeCreateGroupModel">取消</el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+import {
+  Form,
+  FormItem,
+  Input,
+  Select,
+  Option,
+  Radio,
+  RadioGroup
+} from 'element-ui'
+import { getOpenIM } from '@/utils/openIM';
+import { createGroup } from '@/api/group';
+export default {
+  name: 'CreateGroup',
+  components: {
+    ElForm: Form,
+    ElFormItem: FormItem,
+    ElInput: Input,
+    ElSelect: Select,
+    ElOption: Option,
+    ElRadioGroup: RadioGroup,
+    ElRadio: Radio
+  },
+  data() {
+    return {
+      avataruploadUrl:process.env.VUE_APP_BASE_API+"/common/uploadOSS",
+      form: {
+        memberUserIDs:[],
+        adminUserIDs:[],
+        ownerUserID:"",
+        groupInfo:{
+          groupID:"",
+          groupName:"",
+          notification:"",//群公告
+          introduction:"",//群介绍
+          faceURL:"",
+          ex:"",
+          groupType:2,
+          needVerification:0,
+          lookMemberInfo:0,
+          applyMemberFriend:0
+        },
+      },
+      options: [],
+      loading: false,
+      rules: {
+        groupName: [{ required: true, message: '请输入群名称', trigger: 'blur' }]
+      }
+    }
+  },
+  computed: {
+    joinOptionDisabled() {
+      return 2
+    }
+  },
+  created() {
+    this.OpenIM = getOpenIM()
+    this.loadFriendList()
+  },
+  methods: {
+    loadFriendList() {
+      this.loading = true
+      this.OpenIM.getFriendListPage({ offset: 0, count: 100 })
+        .then(({ data }) => {
+          console.log("获取到好友列表", data)
+          this.options = data.map(item => ({
+            label: item.nickname || item.userID,
+            value: item.userID
+          }))
+          this.loading = false
+          this.$store.commit('updateFriendList', data)
+        })
+        .catch(({ errCode, errMsg }) => {
+          this.loading = false
+          this.$message.error(`获取好友列表失败:${errMsg}`)
+        })
+    },
+    onSubmit(ref) {
+      this.$refs[ref].validate(valid => {
+        if (!valid) {
+          return false
+        }
+        this.createGroup()
+      })
+    },
+    closeCreateGroupModel() {
+      this.$store.commit('updateCreateGroupModelVisible', false)
+    },
+    createGroup() {
+      console.log(this.form)
+      createGroup(this.form).then(res => {
+        this.$store.commit('showMessage', {
+          type: 'success',
+          message: '创建群组成功'
+        })
+      })
+        .catch(error => {
+          this.$store.commit('showMessage', {
+            type: 'error',
+            message: error.message
+          })
+        })
+    },
+    checkoutConversation() {
+      console.log("群聊信息",this.groupProfile.groupID)
+
+      //查询会话
+      this.OpenIM.getOneConversation({
+        sourceID: this.groupProfile.groupID,
+        sessionType: 3,
+      }).then(({ data }) => {
+        console.log("查询单挑回话",data)
+        this.$store
+          .dispatch('checkoutConversation', data)
+          .then(() => {
+            this.showDialog = false
+            this.$bus.$emit('checkoutConversation')
+          }).catch(() => {
+          this.$store.commit('showMessage', {
+            message: '没有找到该群聊',
+            type: 'warning'
+          })
+        })
+      })
+    },
+    getOptions() {
+      let options = {
+        ...this.form,
+        memberList: this.form.memberList.map(userID => ({ userID }))
+      }
+      // if ([this.OpenIM.TYPES.GRP_WORK, this.OpenIM.TYPES.GRP_AVCHATROOM].includes(this.form.type)) {
+      //   delete options.joinOption
+      // }
+      return options
+    },
+    handleSearchUser(userID) {
+      if (userID !== '') {
+        this.loading = true
+        this.OpenIM.getUserProfile({ userIDList: [userID] }).then(({ data }) => {
+          this.options = data.map(item => item.userID)
+          this.loading = false
+        })
+          .catch(error => {
+            this.$store.commit('showMessage', {
+              type: 'error',
+              message: error.message
+            })
+          })
+      }
+    },
+    avatarhandleAvatarSuccess(res, file) {
+      if(res.code==200){
+        this.form.groupInfo.faceURL=res.url;
+        this.$forceUpdate();
+      }
+      else{
+        this.msgError(res.msg);
+      }
+    },
+    beforeAvatarUpload(file) {
+      const isLt1M = file.size / 1024 / 1024 < 1;
+      if (!isLt1M) {
+        this.$message.error('上传图片大小不能超过 1MB!');
+      }
+      return   isLt1M;
+    },
+  }
+}
+</script>
+
+<style lang="stylus" scoped>
+</style>

+ 167 - 0
src/components/group/group-content.vue

@@ -0,0 +1,167 @@
+<template>
+  <div class="group-content" v-if="showGroupContent">
+    <div class="group-box">
+      <div class="profile-container">
+        <div class="item-nick text-ellipsis">{{ groupProfile.groupName }}</div>
+        <img
+          class="avatar"
+          :src="groupProfile.faceURL || defaultAvatar"
+        />
+      </div>
+
+      <el-divider></el-divider>
+
+      <p class="content-box">
+        <span class="content-title">群ID</span>
+        <span class="content-text">{{ groupProfile.groupID }}</span>
+      </p>
+
+      <p class="content-box">
+        <span class="content-title">群主</span>
+        <span class="content-text">{{ groupProfile.ownerUserID }}</span>
+      </p>
+
+      <p class="content-box">
+        <span class="content-title">成员数</span>
+        <span class="content-text">{{ groupProfile.memberCount }}</span>
+      </p>
+
+      <p class="content-box">
+        <span class="content-title">公告</span>
+        <span class="content-text">{{ groupProfile.notification || '暂无公告' }}</span>
+      </p>
+
+      <p class="content-box">
+        <span class="content-title">简介</span>
+        <span class="content-text">{{ groupProfile.introduction || '暂无简介' }}</span>
+      </p>
+
+      <el-divider></el-divider>
+
+      <div class="action-buttons">
+        <el-button type="primary" @click="checkoutConversation">发送消息</el-button>
+        <el-button type="danger" @click="quitGroup">退出群组</el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapState } from 'vuex'
+import { getOpenIM } from '@/utils/openIM'
+
+export default {
+  name: 'group-content',
+  data() {
+    return {
+      OpenIM: null,
+      defaultAvatar: 'https://imgcache.qq.com/open/qcloud/video/act/webim-avatar/avatar-2.png'
+    }
+  },
+  computed: {
+    ...mapState({
+      groupContent: state => state.group.groupContent
+    }),
+    showGroupContent() {
+      return Object.keys(this.groupContent).length > 0
+    },
+    groupProfile() {
+      return this.groupContent.group || {}
+    }
+  },
+  created() {
+    console.log("打开次数")
+    this.OpenIM = getOpenIM()
+  },
+  methods: {
+    // checkoutConversation() {
+    //   this.OpenIM.getOneConversation({
+    //     sourceID: this.groupProfile.groupID,
+    //     sessionType: 3 // 2 表示群聊
+    //   }).then(({ data }) => {
+    //     this.$store.dispatch('checkoutConversation', data)
+    //   }).catch(() => {
+    //     this.$store.commit('showMessage', {
+    //       message: '打开群聊失败',
+    //       type: 'error'
+    //     })
+    //   })
+    // },
+    checkoutConversation() {
+      console.log("群聊信息",this.groupProfile.groupID)
+
+      //查询会话
+      this.OpenIM.getOneConversation({
+        sourceID: this.groupProfile.groupID,
+        sessionType: 3,
+      }).then(({ data }) => {
+        console.log("查询单挑回话",data)
+        this.$store
+          .dispatch('checkoutConversation', data)
+          .then(() => {
+            this.showDialog = false
+            this.$bus.$emit('checkoutConversation')
+          }).catch(() => {
+          this.$store.commit('showMessage', {
+            message: '没有找到该群聊',
+            type: 'warning'
+          })
+        })
+      })
+    },
+    quitGroup() {
+      console.log("1234567ssssssss",this.groupContent.group.groupID)
+      this.OpenIM.quitGroup(this.groupContent.group.groupID )
+        .then(() => {
+          this.$store.commit('showMessage', {
+            message: '已退出该群聊',
+            type: 'success'
+          })
+          this.$store.commit('resetGroupContent')
+        })
+        .catch(err => {
+          this.$store.commit('showMessage', {
+            message: err.message || '退出群聊失败',
+            type: 'error'
+          })
+        })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.group-content {
+  padding: 20px;
+  background: #fff;
+  border-radius: 10px;
+  height: 100%;
+  overflow-y: auto;
+}
+.profile-container {
+  display: flex;
+  align-items: center;
+}
+.avatar {
+  width: 60px;
+  height: 60px;
+  border-radius: 8px;
+  margin-right: 15px;
+}
+.content-box {
+  margin-bottom: 10px;
+}
+.content-title {
+  display: inline-block;
+  width: 80px;
+  color: #666;
+}
+.content-text {
+  color: #333;
+}
+.action-buttons {
+  margin-top: 20px;
+  display: flex;
+  justify-content: space-between;
+}
+</style>

+ 98 - 0
src/components/group/group-item.vue

@@ -0,0 +1,98 @@
+<template>
+  <div @click="handleGroupClick" class="scroll-container">
+    <div class="group-item">
+      <avatar :src="group.faceURL" :size="40" />
+      <div class="group-name text-ellipsis">{{ group.groupName }}</div>
+    </div>
+    <group-content v-if="showGroupContent&&isShowGroupContent" />
+  </div>
+
+</template>
+
+<script>
+import { mapState } from 'vuex'
+import GroupItem from './group-item.vue'
+import GroupContent from './group-content.vue'
+import { getOpenIM } from '@/utils/openIM'
+export default {
+  props: ['group'],
+  data() {
+    return {
+      visible: false,
+      options: [
+        {
+          text: '退出群组',
+          handler: this.quitGroup
+        }
+      ]
+    }
+  },
+  components: {
+    GroupItem,
+    GroupContent
+  },
+  computed: {
+    ...mapState({
+      groupList: state => state.group.groupList,
+      groupContent: state => state.group.groupContent
+    }),
+    showGroupContent() {
+      return Object.keys(this.groupContent).length > 0
+    },
+    isShowGroupContent() {
+      console.log(this.groupContent.group.groupID)
+      console.log(this.group.groupID)
+      console.log(this.groupContent.group.groupID === this.group.groupID)
+      return this.groupContent.group.groupID === this.group.groupID
+    }
+  },
+  created() {
+    this.OpenIM = getOpenIM()
+    console.log("群组列表")
+  },
+  methods: {
+    handleGroupClick() {
+      this.$store.dispatch('setGroupContent', {
+        group: this.group,
+        type: 'groupList'
+      })
+      // console.log(this.groupContent.group.groupID)
+
+    },
+    // quitGroup() {
+    //   this.OpenIM.quitGroup(this.group.groupID)
+    //   .catch(error => {
+    //       this.$store.commit('showMessage', {
+    //         type: 'error',
+    //         message: error.message
+    //       })
+    //     })
+    // }
+  }
+}
+</script>
+
+<style lang="stylus" scoped>
+.scroll-container
+  overflow-y scroll
+  flex 1
+  .group-item
+    display flex
+    padding 10px 20px
+    cursor pointer
+    position relative
+    overflow hidden
+    transition .2s
+    &:hover
+      background-color #f5f5f5
+    .avatar
+      width 30px
+      height 30px
+      border-radius 50%
+      margin-right 10px
+      flex-shrink 0
+    .group-name
+      flex 1
+      color #111
+      line-height 30px
+</style>

+ 214 - 0
src/components/group/group-list.vue

@@ -0,0 +1,214 @@
+<template>
+  <div class="list-container">
+    <div class="header-bar">
+      <el-autocomplete
+        :value-key="'groupID'"
+        :debounce="500"
+        size="mini"
+        v-model="groupID"
+        placeholder="输入群名称关键字搜索"
+        :fetch-suggestions="searchGroupByID"
+        class="group-seach-bar"
+        prefix-icon="el-icon-search"
+        :hide-loading="hideSearchLoading"
+        @input="hideSearchLoading = false"
+      ></el-autocomplete>
+      <button title="创建群组1" @click="showCreateGroupModel">
+        <i class="tim-icon-add"></i>
+      </button>
+    </div>
+    <div class="group-container">
+      <group-item v-for="group in groupList" :key="group.groupID" :group="group" />
+      <el-dialog title="创建群组" :visible="createGroupModelVisible" @close="closeCreateGroupModel" width="30%" append-to-body>
+        <create-group></create-group>
+      </el-dialog>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapState } from 'vuex'
+import { Dialog, Autocomplete } from 'element-ui'
+import CreateGroup from './create-group.vue'
+import GroupItem from './group-item.vue'
+import { getOpenIM } from '@/utils/openIM';
+export default {
+  data() {
+    return {
+      groupID: '',
+      hideSearchLoading: true
+    }
+  },
+  components: {
+    GroupItem,
+    ElDialog: Dialog,
+    'create-group': CreateGroup,
+    ElAutocomplete: Autocomplete
+  },
+  computed: {
+    groupList: function() {
+      return this.$store.state.group.groupList
+    },
+    ...mapState({
+      createGroupModelVisible: state => {
+        return state.group.createGroupModelVisible
+      }
+    })
+  },
+  created() {
+    this.OpenIM = getOpenIM()
+  },
+  methods: {
+    onGroupUpdated(groupList) {
+      this.$store.dispatch('updateGroupList', groupList)
+    },
+    createGroup() {},
+    closeCreateGroupModel() {
+      this.$store.commit('updateCreateGroupModelVisible', false)
+    },
+    searchGroupByID(queryString, showInSearchResult) {
+      if (queryString.trim().length > 0) {
+        let request = {
+          keywordList:[queryString],
+          isSearchGroupID:true,
+          isSearchGroupName:true
+        }
+        console.log("搜索群聊请求参数", request)
+        this.hideSearchLoading = false
+        this.OpenIM.searchGroups(request)
+          .then(({ data }) => {
+            console.log("搜索群聊返回参数", data)
+            // 兼容新旧 SDK 返回格式
+            const groupList = Array.isArray(data)
+              ? data
+              : (data.groupList || [])
+
+            if (groupList.length > 0) {
+              this.$store.dispatch('updateGroupList', groupList)
+
+              showInSearchResult(groupList)
+            } else {
+              showInSearchResult([])
+              this.$store.commit('showMessage', {
+                message: '没有找到该群',
+                type: 'error'
+              })
+            }
+          })
+          .catch((err) => {
+            console.error("搜索群聊异常", err)
+            showInSearchResult([])
+            this.$store.commit('showMessage', {
+              message: '搜索群组失败',
+              type: 'error'
+            })
+          })
+
+
+      } else {
+        this.hideSearchLoading = true
+      }
+    },
+    showCreateGroupModel() {
+      this.$store.commit('updateCreateGroupModelVisible', true)
+    },
+    // applyJoinGroup(group) {
+    //   this.tim
+    //     .joinGroup({ groupID: group.groupID })
+    //     .then(async res => {
+    //       switch(res.data.status) {
+    //         case this.OpenIM.TYPES.JOIN_STATUS_WAIT_APPROVAL:
+    //           this.$store.commit('showMessage', {
+    //             message: '申请成功,等待群管理员确认。',
+    //             type: 'info'
+    //           })
+    //           break
+    //         case this.OpenIM.TYPES.JOIN_STATUS_SUCCESS:
+    //           await this.$store.dispatch(
+    //             'checkoutConversation',
+    //             `GROUP${res.data.group.groupID}`
+    //           )
+    //           this.$store.commit('showMessage', {
+    //             message: '加群成功',
+    //             type: 'success'
+    //           })
+    //           break
+    //         case this.OpenIM.TYPES.JOIN_STATUS_ALREADY_IN_GROUP:
+    //           this.$store.commit('showMessage', {
+    //             message: '您已经是群成员了,请勿重复加群哦!',
+    //             type: 'info'
+    //           })
+    //           break
+    //         default: break
+    //       }
+    //     })
+    //     .catch(error => {
+    //       this.$store.commit('showMessage', {
+    //         message: error.message,
+    //         type: 'error'
+    //       })
+    //     })
+    // }
+  }
+}
+</script>
+
+<style lang="stylus" scoped>
+.list-container
+  height 100%
+  width 100%
+  display flex
+  flex-direction column
+  .group-container
+    overflow-y scroll
+  .header-bar
+    display: flex;
+    flex-shrink 0
+    height 50px
+    background-color #f7f7f7
+    border-bottom 1px solid #e5e5e5
+    padding 10px 10px 10px 20px
+    .group-seach-bar
+      width 100%
+      margin-right 10px
+      >>> .el-input
+        input
+          color #111
+          border none
+          border-radius 30px
+          background-color #fff !important
+          &::placeholder
+            color #999
+        .el-icon-search
+          color #999
+    button
+      float right
+      display: inline-block;
+      cursor: pointer;
+      background transparent
+      border: none
+      color: #666;
+      box-sizing: border-box;
+      transition: .3s;
+      -moz-user-select: none;
+      -webkit-user-select: none;
+      -ms-user-select: none;
+      margin: 0
+      padding 0
+      width 30px
+      height 30px
+      line-height 34px
+      font-size: 24px;
+      text-align: center;
+      white-space: nowrap;
+      border-radius: 50%
+      outline 0
+      flex-shrink 0
+      &:hover
+        transform: rotate(360deg);
+        color $light-primary
+  .scroll-container
+    overflow-y scroll
+    flex 1
+
+</style>

+ 167 - 0
src/components/group/member-profile-card.vue

@@ -0,0 +1,167 @@
+<template>
+  <transition name="el-fade-in">
+    <div
+      class="member-profile-card-wrapper"
+      ref="member-profile-card"
+      v-show="visible"
+      :style="{top: y + 'px', left: x + 'px'}"
+    >
+      <div class="profile">
+        <avatar :src="member.avatar" class="avatar" :size="44" />
+        <div class="basic">
+          <span>ID:{{member.userID}}</span>
+          <span>昵称:{{member.nick||"暂无"}}</span>
+        </div>
+      </div>
+      <el-divider class="divider" />
+      <div class="member-profile">
+        <div class="item">
+          <span class="label">群名片</span>
+          {{member.nameCard||"暂无"}}
+        </div>
+        <div class="item">
+          <span class="label">入群时间</span>
+          {{joinTime}}
+        </div>
+        <div v-if="member.muteUntil" class="item">
+          <span class="label">禁言至</span>
+          {{muteUntil}}
+        </div>
+      </div>
+      <el-button
+        class="send-message-btn"
+        type="primary"
+        size="mini"
+        title="发消息"
+        @click="handleSendMessage"
+        icon="el-icon-message"
+        circle
+      ></el-button>
+    </div>
+  </transition>
+</template>
+
+<script>
+import { Divider } from 'element-ui'
+import { getFullDate } from '../../utils/date'
+
+// 群成员资料卡片组件,全局共用同一个组件。
+export default {
+  name: 'MemberProfileCard',
+  components: {
+    ElDivider: Divider
+  },
+  data() {
+    return {
+      member: {},
+      x: 0, // 显示的位置 x
+      y: 0, // 显示的位置 y
+      visible: false
+    }
+  },
+  mounted() {
+    // 通过事件总线,监听 showMemebrProfile 事件
+    this.$bus.$on('showMemberProfile', this.handleShowMemberProfile, this)
+  },
+  computed: {
+    joinTime() {
+      if (this.member.joinTime) {
+        return getFullDate(new Date(this.member.joinTime * 1000))
+      }
+      return ''
+    },
+    muteUntil() {
+      if (this.member.muteUntil) {
+        return getFullDate(new Date(this.member.muteUntil * 1000))
+      }
+      return ''
+    }
+  },
+  methods: {
+    handleSendMessage() {
+      this.$store.dispatch('checkoutConversation', `C2C${this.member.userID}`)
+      this.hide()
+    },
+    handleShowMemberProfile({ event, member }) {
+      // 可以拿到 meber 和 点击事件的 event 信息
+      this.member = member || {}
+      this.x = event.x
+      this.y = event.y
+      this.show()
+    },
+    show() {
+      if (this.visible) {
+        return
+      }
+      // 显示时,监听全局点击事件,若点击区域不是当前组件,则隐藏
+      window.addEventListener('click', this.handleClick, this)
+      this.visible = true
+    },
+    hide() {
+      if (!this.visible) {
+        return
+      }
+      // 隐藏时,注销监听
+      window.removeEventListener('click', this.handleClick, this)
+      this.visible = false
+    },
+    handleClick(event) {
+      // 判断点击区域是否是当前组件,若不是,则隐藏组件
+      if (event.target !== this.$refs['member-profile-card']) {
+        this.hide()
+      }
+    }
+  }
+}
+</script>
+
+<style lang="stylus" scoped>
+.member-profile-card-wrapper {
+  max-width: 300px;
+  padding: 24px;
+  background: #fff;
+  border-radius: 5px;
+  position: fixed;
+  box-shadow: 0 0 10px gray;
+
+  .profile {
+    display: flex;
+
+    .avatar {
+      width: 60px;
+      height: 60px;
+      min-width 60px;
+      margin-right: 12px;
+    }
+
+    .basic {
+      display: flex;
+      align-items: flex-start;
+      flex-direction: column;
+    }
+  }
+
+  .divider {
+    margin: 12px 0;
+  }
+
+  .member-profile {
+    margin-bottom: 12px;
+
+    .item {
+      font-size: 15px;
+
+      .label {
+        display: inline-block;
+        width: 4em;
+        text-align: justify;
+        text-align-last: justify;
+        color: gray;
+      }
+    }
+  }
+  .send-message-btn {
+    float right
+  }
+}
+</style>

+ 6 - 4
src/components/layout/side-bar.vue

@@ -25,7 +25,7 @@
             <template v-else>{{applicationUnreadCount}}</template>
           </sup>
         </div>
-        <div
+        <!-- <div
           id="add-friend"
            class="iconfont icon-smile"
           :class="{ active: showAddFriend }"
@@ -41,7 +41,7 @@
           class="iconfont icon-group"
           :class="{ active: showGroupList }"
           title="群组列表"
-        ></div>
+        ></div> -->
 
         <!--  <div
            id="black-list"
@@ -245,6 +245,7 @@ export default {
     width: 80px;
     height: 80vh;
     background-color: #303841;
+    overflow: hidden;
 
     .tab-items {
       display: flex;
@@ -276,7 +277,7 @@ export default {
           z-index: 0;
           height: 70px;
           // border-left 4px solid $border-highlight
-          border-left: 4px solid #5cadff;
+          border-left: 4px solid #07c160;
         }
       }
 
@@ -343,7 +344,8 @@ export default {
     min-width: 0;
     height: 80vh;
     position: relative;
-    background-color: #363e47;
+    /* 列表区使用柔和浅灰,避免纯白过亮 */
+    background-color: #eef1f4;
   }
   .group-live {
     position relative

+ 1 - 1
src/components/message/merger-message/message-merger.vue

@@ -6,7 +6,7 @@
           <div   v-for="(messageItem, index) in mergerList(mergerMessage)" :key="index">
             <div class="message-item">
               <div class="avatar-box">
-                <avatar class="group-member-avatar" :src="messageItem.senderFaceUrl"/>
+                <avatar class="group-member-avatar" :src="messageItem.senderFaceUrl" :size="36"/>
               </div>
               <div class="container-box">
                 <div class="nick-date">

+ 48 - 20
src/components/message/message-bubble.vue

@@ -1,13 +1,15 @@
 <template>
   <div class="chat-bubble" @mousedown.stop @contextmenu.prevent>
     <el-dropdown trigger="" ref="dropdown" placement="bottom-start" v-if="!message.isRevoked" @command="handleCommand">
-      <div style="display: flex">
-        <div v-if="isMine && messageReadByPeer" class="message-status">
-          <span>{{messageReadByPeer}}</span>
-        </div>
-        <div v-if="message.contentType!==2101" class="message-content" :class="bubbleStyle">
+      <div class="bubble-stack" :class="{ 'bubble-stack--mine': isMine }">
+        <div
+          v-if="message.contentType!==2101"
+          class="message-content"
+          :class="bubbleClassNames"
+        >
           <slot></slot>
         </div>
+        <div v-if="isMine && messageReadByPeer" class="message-read-receipt">{{ messageReadByPeer }}</div>
       </div>
       <el-dropdown-menu slot="dropdown">
         <el-dropdown-item command="revoke" v-if="isMine&&!isTimeout">撤回</el-dropdown-item>
@@ -50,7 +52,12 @@
       message: {
         type: Object,
         required: true
-      }
+      },
+      /** default | media  图片/视频等用 media,贴近微信:无绿底气泡、无小尾巴 */
+      bubbleVariant: {
+        type: String,
+        default: 'default',
+      },
     },
     created() {
       this.OpenIM = getOpenIM();
@@ -77,9 +84,13 @@
           classString += 'message-received'
         }
         if (this.isNew) {
-          classString += 'new'
+          classString += ' new'
         }
-        return classString
+        return classString.trim()
+      },
+      bubbleClassNames() {
+        const extra = this.bubbleVariant === 'media' ? 'message-bubble--media' : ''
+        return [this.bubbleStyle, extra].filter(Boolean).join(' ')
       },
       text() {
         if (this.message.sessionType === 1 && !this.isMine) {
@@ -202,15 +213,18 @@
     }
   }
 .chat-bubble
-  /*position relative*/
-  .message-status
-    display: flex;
-    min-width: 25px;
-    margin-right: 10px;
-    justify-content: center;
-    align-items: center;
-    font-size: 12px;
-    color: #6e7981;
+  .bubble-stack
+    display flex
+    flex-direction column
+    max-width 100%
+    &--mine
+      align-items flex-end
+  .message-read-receipt
+    font-size 12px
+    color #909399
+    margin-top 4px
+    line-height 1.2
+    padding-right 2px
   .message-content
     outline: none
     font-size 14px
@@ -248,18 +262,32 @@
       transform: scale(0);
       transform-origin: top left;
       animation: bounce 500ms linear both;
+  /* 微信 PC 己方气泡:浅绿底 + 深色字 */
   .message-send
-    background-color #5cadff
+    background-color #95ec69
     margin-right 15px
     border-radius 4px 0 4px 4px
-    color #fff
+    color #111
+    span
+      text-shadow none
     &::before
       right: -10px
-      color #5cadff
+      color #95ec69
     &.new
       transform: scale(0);
       transform-origin: top right;
       animation: bounce 500ms linear both;
+  /* 图片/视频:不要绿/白气泡壳,圆角直接压在图上(贴近微信 PC) */
+  .message-content.message-bubble--media
+    padding 0
+    box-shadow none
+    background-color transparent
+    &::before
+      display none
+    &.message-send
+      color inherit
+    &.message-received
+      background-color transparent
   .el-dropdown {
     vertical-align: top;
     display flex

+ 10 - 2
src/components/message/message-elements/image-element.vue

@@ -1,5 +1,5 @@
 <template>
-  <message-bubble :isMine=isMine :message=message>
+  <message-bubble :isMine="isMine" :message="message" bubble-variant="media">
      <!-- el-image在IE下会自动加上用于兼容object-fit的类,该类的样式在没设置图片宽高是会 GG -->
     <img class="image-element" :src="imageUrl" @load="onImageLoaded" @click="handlePreview" />
     <el-progress
@@ -67,7 +67,15 @@ export default {
 
 <style lang="stylus" scoped>
 .image-element
-  max-width 250px
+  max-width 260px
+  width auto
+  height auto
+  max-height 360px
+  display block
   cursor zoom-in
+  border-radius 4px
+  vertical-align bottom
+  @media (max-width: 600px)
+    max-width 72vw
 
 </style>

+ 14 - 1
src/components/message/message-elements/video-element.vue

@@ -1,10 +1,12 @@
 <template>
-  <message-bubble :isMine=isMine :message=message>
+  <message-bubble :isMine="isMine" :message="message" bubble-variant="media">
     <video
       :src="payload.videoUrl"
       controls
       class="video"
       @error="videoError"
+      @loadedmetadata="onMediaReady"
+      @loadeddata="onMediaReady"
     ></video>
     <el-progress
       v-if="showProgressBar"
@@ -49,12 +51,23 @@ export default {
     videoError(e) {
       this.$store.commit('showMessage', { type: 'error', message: '视频出错,错误原因:' + e.target.error.message })
     },
+    /** 视频元数据/首帧加载后尺寸会变大,需要通知聊天容器重新滚底,避免被输入框挡住 */
+    onMediaReady(event) {
+      try {
+        if (this.$bus && typeof this.$bus.$emit === 'function') {
+          this.$bus.$emit('image-loaded', event)
+        }
+      } catch (e) {
+        /* ignore */
+      }
+    },
   }
 }
 </script>
 
 <style lang="stylus" scoped>
 .video
+  max-width 260px
   width 100%
   max-height 300px
 </style>

+ 2 - 2
src/components/message/message-footer.vue

@@ -8,6 +8,7 @@
 <script>
 import { mapState } from 'vuex'
 import { getFullDate } from '../../utils/date'
+import { imUserIdEqual } from '@/utils/common'
 export default {
   name: 'MessageFooter',
   props: {
@@ -43,8 +44,7 @@ export default {
       return this.message.nick || this.message.from
     },
     isMine() {
-
-      return this.message.sendID ===  this.$store.getters.userID
+      return imUserIdEqual(this.message.sendID, this.$store.getters.userID)
     }
   }
 }

+ 3 - 2
src/components/message/message-header.vue

@@ -8,6 +8,7 @@
 <script>
 import { mapState } from 'vuex'
 import { getFullDate } from '../../utils/date'
+import { imUserIdEqual } from '@/utils/common'
 export default {
   name: 'MessageHeader',
   props: {
@@ -31,7 +32,7 @@ export default {
       if (this.isMine) {
         return this.message.senderNickname || this.currentUserProfile.userID
       }
-      if (isC2C&&this.message.sendID !==  this.$store.getters.userID&&this.message.sendID !== "imAdmin") {
+      if (isC2C && !imUserIdEqual(this.message.sendID, this.$store.getters.userID) && this.message.sendID !== 'imAdmin') {
         return (
           this.message.senderNickname ||
           this.message.sendID
@@ -45,7 +46,7 @@ export default {
       return this.message.nameCard ||  this.message.nick || this.message.from
     },
     isMine() {
-      return this.message.sendID ===  this.$store.getters.userID
+      return imUserIdEqual(this.message.sendID, this.$store.getters.userID)
     }
   }
 }

+ 15 - 11
src/components/message/message-item.vue

@@ -7,7 +7,7 @@
     >
       <div class="col-1" v-if="showAvatar">
         <!-- 头像 -->
-        <avatar :src="avatar" />
+        <avatar :src="avatar" :size="48" />
       </div>
       <div class="col-2">
         <!-- 消息主体 -->
@@ -100,7 +100,7 @@
     >
       <!-- 头像 群组没有获取单个头像的接口,暂时无法显示头像-->
       <div class="col-1" v-if="showAvatar" >
-        <avatar class="group-member-avatar" :src="avatar" @click.native="showGroupMemberProfile"/>
+        <avatar class="group-member-avatar" :src="avatar" :size="48" @click.native="showGroupMemberProfile"/>
       </div>
       <div class="col-2">
         <!-- 消息主体 -->
@@ -194,7 +194,7 @@
 
     <div class="system-layout" v-if="currentConversationType === 4 ">
       <div class="col-1">
-        <avatar :src="avatar" :type="currentConversationType" />
+        <avatar :src="avatar" :type="currentConversationType" :size="48" />
       </div>
       <div class="col-2">
         <message-header :message="message" />
@@ -221,6 +221,7 @@
   import GeoElement from './message-elements/geo-element.vue'
   import MergerElement from './message-elements/merger-element.vue'
   import AtTextElement from './message-elements/at-element.vue'
+  import { imUserIdEqual } from '@/utils/common'
   export default {
     name: 'MessageItem',
     props: {
@@ -282,21 +283,22 @@
       },
       avatar() {
         if (this.currentConversation.conversationType === 1) {
-          return this.message.senderFaceUrl
+          if (this.isMine) {
+            return this.currentUserProfile.faceURL || this.currentUserProfile.faceUrl || ''
+          }
+          return this.message.senderFaceUrl || this.message.faceURL || ''
         } else if (this.currentConversation.conversationType === 3) {
           return this.isMine
-            ? this.currentUserProfile.faceUrl
-            : this.message.faceUrl
-        } else {
-          return ''
+            ? (this.currentUserProfile.faceURL || this.currentUserProfile.faceUrl || '')
+            : (this.message.faceURL || this.message.faceUrl || '')
         }
+        return ''
       },
       currentConversationType() {
         return this.currentConversation.conversationType
       },
       isMine() {
-        // console.log(this.currentUserProfile, this.currentConversation);
-        return this.message.sendID ===  this.$store.getters.userID
+        return imUserIdEqual(this.message.sendID, this.$store.getters.userID)
       },
       messagePosition() {
         if (
@@ -347,7 +349,7 @@
 
 <style lang="stylus" scoped>
 .message-wrapper {
-  margin: 20px 0;
+  margin: 14px 0;
 
   .content-wrapper {
     display: flex;
@@ -359,6 +361,8 @@
   display: flex;
 
   .col-1 {
+    flex-shrink: 0;
+    align-self: flex-start;
     .avatar {
       width: 56px;
       height: 56px;

+ 76 - 44
src/components/message/message-send-box.vue

@@ -18,7 +18,7 @@
       <!-- <i class="iconfont icon-zidingyi" title="发自定义消息" @click="sendCustomDialogVisible = true"></i> -->
 
       <!-- <i class="iconfont icon-diaocha" title="小调查" @click="surveyDialogVisible = true"></i> -->
-      <el-dropdown>
+      <!-- <el-dropdown>
       <span class="el-dropdown-link" v-if="currentConversationType !== 3">
       <i class="el-icon-phone-outline" v-if="toAccount !== userID&&((imType==1&&orderType==2)||imType==2)" title="语音通话"></i>
       <i class="el-icon-phone-outline" title="语音通话"></i>
@@ -27,15 +27,15 @@
           <el-dropdown-item  @click.native="trtcCalling('video')">视频通话</el-dropdown-item>
           <el-dropdown-item  @click.native="trtcCalling('audio')">语音通话</el-dropdown-item>
         </el-dropdown-menu>
-      </el-dropdown>
+      </el-dropdown> -->
       <div class="group-live-icon-box" v-if="currentConversationType === 4&& groupProfile.type !== 'AVChatRoom'" title="群直播" @click="groupLive">
         <i class="group-live-icon"></i>
         <i class="group-live-icon-hover"></i>
       </div>
-      <i class="el-icon-s-order"  title="疗法" @click="handlePackageList()"></i>
+      <!-- <i class="el-icon-s-order"  title="疗法" @click="handlePackageList()"></i>
       <i class="el-icon-tickets"  title="药品订单" @click="handleStoreOrder()"></i>
       <i class="el-icon-edit-outline"  title="会诊" @click="handlePrescribe()"></i>
-      <i class="el-icon-edit-outline"  title="私域疗法券" @click="handleCoupon()"></i>
+      <i class="el-icon-edit-outline"  title="私域疗法券" @click="handleCoupon()"></i> -->
       <!--<i class="el-icon-document" v-if="imType==1" title="诊断报告" @click="handleInquiryReport()"></i>
       <i class="el-icon-finished" v-if="imType==2" title="随访单" @click="handleFollow()"></i>
       <i class="el-icon-edit-outline" v-if="imType==2" title="开报告" @click="handleDrugReport()"></i>
@@ -840,12 +840,9 @@ export default {
               ex:""
             }
           }
-          this.OpenIM.sendMessage(sendText).then(({ data }) => {
-            console.log("发送消息返回参数",data)
-            const msgList = Array.isArray(data) ? data : [data];
-            this.$store.commit('pushCurrentMessageList', msgList)
-            this.updateConversationList()
-            this.$bus.$emit('scroll-bottom')
+          this.OpenIM.sendMessage(sendText).then((res) => {
+            console.log('发送@消息返回', res)
+            this.commitPushedMessagesFromSendResult(res, data)
           }).catch(({ errCode, errMsg }) => {
             // 调用失败
           });
@@ -886,14 +883,9 @@ export default {
           console.warn('⚠️ OpenIM SDK 尚未登录完成,无法发起邀请');
           return;
         }*/
-        this.OpenIM.sendMessage(sendText).then(({ data }) => {
-          console.log("发送消息返回参数",data)
-          // 调用成功
-          console.log("customData",customData)
-          const msgList = Array.isArray(data) ? data : [data];
-          this.$store.commit('pushCurrentMessageList', msgList)
-          this.updateConversationList()
-          this.$bus.$emit('scroll-bottom')
+        this.OpenIM.sendMessage(sendText).then((res) => {
+          console.log('发送文本消息返回', res)
+          this.commitPushedMessagesFromSendResult(res, data)
         }).catch(({ errCode, errMsg }) => {
           // 调用失败
         });
@@ -909,6 +901,46 @@ export default {
     }*/
 
 
+    },
+    /**
+     * sendMessage 成功回调里 data 可能为 undefined,或与文档不一致;unwrap 在 store.pushCurrentMessageList 中处理。
+     * @param {*} res SDK 返回值(常见 { data: Message })
+     * @param {*} fallbackMessage 发送前 createXxxMessage 得到的本地消息(兜底上屏)
+     */
+    commitPushedMessagesFromSendResult(res, fallbackMessage) {
+      let raw =
+        res && Object.prototype.hasOwnProperty.call(res, 'data')
+          ? res.data
+          : res
+      // 服务端回执可能为空 / undefined / 不是消息对象;此时使用本地 createXxxMessage 兜底
+      let list = []
+      if (Array.isArray(raw)) {
+        list = raw.filter(Boolean)
+      } else if (raw && typeof raw === 'object' && (raw.clientMsgID || raw.serverMsgID || raw.contentType)) {
+        list = [raw]
+      }
+      if (!list.length && fallbackMessage) {
+        list = [fallbackMessage]
+      }
+      console.log('[sendMessage] commit 本端消息:', list, '(原始回执:', res, ')')
+      if (!list.length) {
+        return
+      }
+      // 本端发送:直接追加,不走归属过滤(避免 conversationID 命名差异导致被误过滤)
+      this.$store.commit('appendCurrentMessage', list)
+      this.updateConversationList()
+      const safeEmitScroll = () => {
+        try {
+          if (this.$bus && typeof this.$bus.$emit === 'function') {
+            this.$bus.$emit('scroll-bottom')
+          }
+        } catch (e) {
+          console.warn('[scroll-bottom] emit 失败', e)
+        }
+      }
+      this.$nextTick(safeEmitScroll)
+      setTimeout(safeEmitScroll, 50)
+      setTimeout(safeEmitScroll, 300)
     },
     updateConversationList(){
       this.OpenIM.getAllConversationList()
@@ -1111,15 +1143,11 @@ export default {
                 operatorUserID:"",
                 ex:""
               }
-            });
-          })
-          .then(({ data }) => {
-            console.log('发送图片成功', data);
-            const msgList = Array.isArray(data) ? data : [data];
-            this.$store.commit('pushCurrentMessageList', msgList);
-            this.updateConversationList()
-            this.$bus.$emit('scroll-bottom');
-            this.$refs.imagePicker.value = null;
+            }).then((res) => {
+              console.log('发送图片成功', res)
+              this.commitPushedMessagesFromSendResult(res, message)
+              this.$refs.imagePicker.value = null
+            })
           })
           .catch(({ errCode, errMsg }) => {
             console.error('发送图片失败', errCode, errMsg);
@@ -1173,13 +1201,10 @@ export default {
           }
         };
 
-        return this.OpenIM.sendMessage(sendFile);
-      }).then(({ data }) => {
-        const msgList = Array.isArray(data) ? data : [data];
-        this.$store.commit('pushCurrentMessageList', msgList);
-        this.updateConversationList()
-        this.$bus.$emit('scroll-bottom');
-        this.$refs.filePicker.value = null;
+        return this.OpenIM.sendMessage(sendFile).then((res) => {
+          this.commitPushedMessagesFromSendResult(res, data)
+          this.$refs.filePicker.value = null
+        })
       }).catch(({ errCode, errMsg }) => {
         console.error("发送文件失败", errCode, errMsg);
         this.$store.commit('showMessage', {
@@ -1261,12 +1286,9 @@ export default {
               }
             };
 
-            this.OpenIM.sendMessage(sendVideo).then(({ data }) => {
-              const msgList = Array.isArray(data) ? data : [data];
-              this.$store.commit('pushCurrentMessageList', msgList);
-              this.updateConversationList();
-              this.$bus.$emit('scroll-bottom');
-              this.$refs.videoPicker.value = null;
+            this.OpenIM.sendMessage(sendVideo).then((res) => {
+              this.commitPushedMessagesFromSendResult(res, data)
+              this.$refs.videoPicker.value = null
             }).catch(({ errCode, errMsg }) => {
               console.error("发送视频失败", errCode, errMsg);
             });
@@ -1314,8 +1336,10 @@ export default {
 #message-send-box-wrapper {
   box-sizing: border-box;
   overflow: hidden;
-  padding: 3px 20px 20px 20px;
-  width : 100%;
+  padding: 8px 16px 16px;
+  width: 100%;
+  /* 微信 PC:输入区浅灰,聚焦时由 :style 变白 */
+  background-color: #f5f5f5;
 }
 
 .emojis {
@@ -1373,16 +1397,24 @@ textarea {
 .bottom {
   padding-top: 10px;
   position: relative;
+  padding-bottom: 6px;
+  min-height: 72px;
+
+  .text-input {
+    padding-right: 44px;
+    box-sizing: border-box;
+  }
 
   .btn-send {
     cursor: pointer;
     position: absolute;
     color: $primary;
     font-size: 30px;
-    right: 0;
-    bottom: -5px;
+    right: 4px;
+    bottom: 8px;
     padding: 6px 6px 4px 4px;
     border-radius: 50%;
+    z-index: 2;
   }
 }
 .group-live-icon-box {

+ 2 - 2
src/components/message/trtc-calling/group-member-list.vue

@@ -12,7 +12,7 @@
             :label="JSON.stringify({ userID: 'AtAllTag', nick: '所有人' })"
           >
             <div class="group-member">
-              <avatar :src="''" />
+              <avatar :src="''" :size="40" />
               <div class="member-name text-ellipsis">
                 <span>所有人</span>
               </div>
@@ -28,7 +28,7 @@
           <!--                    </el-checkbox>-->
           <el-checkbox v-for="member in members" :disabled="member.userID===userID" :label="JSON.stringify({userID:member.userID,nick:member.nickname || member.userID})" :key="member.userID">
             <div class="group-member">
-              <avatar  :src="member.faceURL" />
+              <avatar :src="member.faceURL" :size="40" />
               <div class="member-name text-ellipsis">
                 <span v-if="member.nameCard" >{{ member.nameCard }}</span>
                 <span v-else-if="member.nickname" >{{ member.nickname }}</span>

+ 145 - 0
src/components/my-profile.vue

@@ -0,0 +1,145 @@
+<template>
+  <div class="my-profile-wrapper">
+    <el-dialog title="编辑个人资料" :visible.sync="showEditMyProfile" width="30%">
+      <el-form v-model="form" label-width="100px">
+        <el-form-item label="头像">
+          <el-input v-model="form.avatar" placeholder="头像地址(URL)" />
+        </el-form-item>
+        <el-form-item label="昵称">
+          <el-input v-model="form.nick" placeholder="昵称" />
+        </el-form-item>
+        <el-form-item label="性别">
+          <el-radio-group v-model="form.gender">
+            <el-radio :label="1">男</el-radio>
+            <el-radio :label="0">女</el-radio>
+            <el-radio :label="3">不显示</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="好友申请">
+          <el-radio-group v-model="form.allowType">
+            <!--<el-radio :label="OpenIM.TYPES.ALLOW_TYPE_ALLOW_ANY">允许直接加为好友</el-radio>
+            <el-radio :label="OpenIM.TYPES.ALLOW_TYPE_NEED_CONFIRM">需要验证</el-radio>
+            <el-radio :label="OpenIM.TYPES.ALLOW_TYPE_DENY_ANY">不允许任何人添加好友</el-radio>-->
+          </el-radio-group>
+        </el-form-item>
+      </el-form>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="showEditMyProfile = false">取 消</el-button>
+        <el-button type="primary" @click="editMyProfile">确 定</el-button>
+      </span>
+    </el-dialog>
+    <el-popover :width="200" trigger="click" placement="right" class="popover">
+      <profile-card :profile="currentUserProfile" />
+      <!-- <i class="el-icon-setting edit-my-profile" @click="handleEdit"></i> -->
+      <avatar
+        slot="reference"
+        :src="currentUserProfile.faceURL"
+        class="my-avatar"
+        :size="50"
+      />
+    </el-popover>
+  </div>
+</template>
+
+<script>
+import { Popover, Form, FormItem, RadioGroup, Radio } from 'element-ui'
+import { mapState } from 'vuex'
+import ProfileCard from './profile-card'
+export default {
+  name: 'MyProfile',
+  components: {
+    ElPopover: Popover,
+    ProfileCard,
+    ElForm: Form,
+    ElFormItem: FormItem,
+    ElRadioGroup: RadioGroup,
+    ElRadio: Radio
+  },
+  data() {
+    return {
+      showEditMyProfile: false,
+      form: { avatar: '', nick: '', gender: '',allowType: '' },
+      OpenIM:null
+    }
+  },
+  computed: {
+    ...mapState({
+      currentUserProfile: state => state.imuser.currentUserProfile,
+      currentConversation: state => state.conversation.currentConversation
+    }),
+    gender() {
+      switch (this.currentUserProfile.gender) {
+        case 1:
+          return '男'
+        case 0:
+          return '女'
+        default:
+          return '暂无'
+      }
+    }
+  },
+  methods: {
+    editMyProfile() {
+      if (this.form.avatar && this.form.avatar.indexOf('http') === -1) {
+        this.$store.commit('showMessage', {
+          message: '头像应该是 Url 地址',
+          type: 'warning'
+        })
+        this.form.avatar = ''
+        return
+      }
+      const options = {}
+      // 过滤空串
+      Object.keys(this.form).forEach(key => {
+        if (this.form[key]) {
+          options[key] = this.form[key]
+        }
+      })
+      this.tim
+        .updateMyProfile(options)
+        .then(() => {
+          this.$store.commit('showMessage', {
+            message: '修改成功'
+          })
+          this.showEditMyProfile = false
+        })
+        .catch(imError => {
+          this.$store.commit('showMessage', {
+            message: imError.message,
+            type: 'error'
+          })
+        })
+    },
+    handleEdit() {
+      const { avatar, nick, gender, allowType } = this.currentUserProfile
+      Object.assign(this.form, { avatar, nick, gender, allowType })
+      this.showEditMyProfile = true
+    }
+  }
+}
+</script>
+
+<style lang="stylus" scoped>
+.my-profile-wrapper
+  width 50px
+  height 50px
+  margin 15px
+  &>span
+    display: block;
+    width: 100%;
+    height: 100%;
+  .popover
+    z-index:101;
+    padding none
+    border none
+    border-radius 30px
+.my-avatar
+  cursor pointer
+  border-radius: 50%;
+
+.edit-my-profile
+  position absolute
+  top 10px
+  right 10px
+  cursor pointer
+</style>

+ 79 - 0
src/components/profile-card.vue

@@ -0,0 +1,79 @@
+<template>
+  <div class="profile-card-wrapper">
+    <div class="content">
+      <avatar :src="profile.faceURL" :size="48" />
+      <div class="basic">
+        <span class="nick text-ellipsis">销售:{{ profile.nickname || profile.userID }}</span>
+        <!-- <span class="iconfont" :class="genderClass"></span> -->
+      </div>
+      <div><span class="nick text-ellipsis">{{ profile.userID }}</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'ProfileCard',
+  props: {
+    profile: {
+      type: Object,
+      required: true
+    }
+  },
+  computed: {
+    genderClass() {
+      switch (this.profile.gender) {
+        case 1:
+          return 'icon-male'
+        case 0:
+          return 'icon-female'
+        default:
+          return '暂无'
+      }
+    }
+  }
+}
+</script>
+
+<style lang="stylus" scpoed>
+.profile-header {
+  display: flex;
+  margin-bottom: 12px;
+}
+
+.profile-card-wrapper {
+  .content {
+    display: flex;
+    justify-content: center;
+    flex-direction: column;
+    align-items: center;
+  }
+
+  .avatar {
+    width: 70px;
+    height: 70px;
+  }
+}
+
+.basic {
+  display: flex;
+  align-items: center;
+  margin-top: 12px;
+}
+
+.icon-male {
+  color: $primary;
+}
+
+.icon-female {
+  color: #ff8096;
+}
+
+.nick {
+  font-size: 18px;
+  margin-right: 8px;
+  max-width: 100px;
+  display: inline-block;
+}
+</style>

+ 80 - 0
src/components/qr-code-list.vue

@@ -0,0 +1,80 @@
+<template>
+  <div class="qr-code-list-wrapper">
+    <div class="header">
+      其他体验方式 |
+      <a
+        href="https://cloud.tencent.com/product/im"
+        class="link"
+        target="_blank"
+      >访问官网</a>
+    </div>
+    <div class="qr-code-wrapper">
+      <div class="qr-code-item">
+        <img
+          src="https://cloudcache.tencent-cloud.com/open_proj/proj_qcloud_v2/gateway/product/im-new/css/img/applets.png"
+        />
+        <div class="text">小程序</div>
+      </div>
+      <div class="qr-code-item">
+        <img
+                src="https://upload-dianshi-1255598498.file.myqcloud.com/gh_47854f5a3fb8_258-b007e3dc9f40667fdcfcac4c4476c7ce276c7d0e.jpg"
+        />
+        <div class="text">直播电商解决方案</div>
+      </div>
+      <div class="qr-code-item">
+        <img src="https://main.qcloudimg.com/raw/73daa40cef967ba415cdba2cabf4bdc7.png" />
+        <div class="text">iOS</div>
+      </div>
+      <div class="qr-code-item">
+        <img
+          src="https://cloudcache.tencent-cloud.com/open_proj/proj_qcloud_v2/gateway/product/im-new/css/img/android.png"
+        />
+        <div class="text">Android</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+
+export default {
+  name: 'qr-code-list',
+  mounted() {
+  },
+  methods: {
+
+  }
+}
+</script>
+
+<style scoped>
+.qr-code-list-wrapper {
+  /*width: 400px;*/
+}
+.header {
+  color: #fff;
+  margin: 16px 0;
+}
+.link {
+  color: #38c9ff;
+  text-decoration: none;
+}
+.qr-code-wrapper {
+  display: flex;
+  justify-content: space-between;
+}
+.qr-code-item {
+ margin-right: 20px;
+}
+
+.qr-code-item .text {
+  font-size: 15px;
+  text-align: center;
+  color: #fff;
+}
+.qr-code-item img {
+  width: 120px;
+  height: 120px;
+  border-radius: 5px;
+}
+</style>

+ 5 - 6
src/components/user/login.vue

@@ -15,7 +15,7 @@
 
 <script>
   import logo from '../../assets/image/logo.png'
-  import { getOpenIM,getCbEvents } from '@/utils/openIM';
+  import { getOpenIM, getCbEvents, getOpenIMServerConfig } from '@/utils/openIM';
   import { accountCheck } from '@/api/company/companyUser';
   export default {
     name: 'Login',
@@ -72,11 +72,10 @@
           const config = {
             userID: this.$store.getters.userID,
             token: this.userToken,
-            logLevel:0,
-            platformID: 5, // 使用配置的平台ID
-            apiAddr: 'https://web.im.cdwjyyh.com/api', // API地址
-            wsAddr: 'wss://web.im.cdwjyyh.com/msg_gateway', // WebSocket地址
-            dataDir: '/imdata' // 添加数据存储目录
+            logLevel: 0,
+            platformID: 5,
+            ...getOpenIMServerConfig(),
+            dataDir: '/imdata',
           }
           console.log("config",config)
           console.log("userToken",this.userToken)

+ 4 - 0
src/constant/call.js

@@ -0,0 +1,4 @@
+export const CALL_TYPE = {
+  AUDIO_CALL: 'audio',
+  VIDEO_CALL: 'video'
+}

+ 116 - 0
src/layout/index.vue

@@ -12,6 +12,20 @@
         <settings />
       </right-panel>
     </div>
+    <IM ref="imPanel" class="im-box" v-show="imOpen" />
+    <div
+      class="newIm"
+      @click="openNewIm"
+      @mouseenter="handleMouseEnter"
+      @mouseleave="handleMouseLeave"
+      :style="{ transform: `translateX(${offset}px)` }"
+      title="打开即时聊天"
+    >
+      <svg-icon icon-class="system" class-name="newImIcon" />
+      <div class="num" v-if="totalUnreadCount > 0">
+        {{ totalUnreadCount > 99 ? '99+' : totalUnreadCount }}
+      </div>
+    </div>
   </div>
 </template>
 
@@ -20,11 +34,14 @@ import RightPanel from '@/components/RightPanel'
 import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
 import ResizeMixin from './mixin/ResizeHandler'
 import { mapState } from 'vuex'
+import { mapGetters } from 'vuex'
 import variables from '@/assets/styles/variables.scss'
+import IM from '@/views/im/index.vue'
 
 export default {
   name: 'Layout',
   components: {
+    IM,
     AppMain,
     Navbar,
     RightPanel,
@@ -34,6 +51,9 @@ export default {
   },
   mixins: [ResizeMixin],
   computed: {
+    ...mapGetters([
+      'totalUnreadCount'
+    ]),
     ...mapState({
       theme: state => state.settings.theme,
       sideTheme: state => state.settings.sideTheme,
@@ -55,8 +75,57 @@ export default {
     }
   },
   methods: {
+    openNewIm() {
+      this.imOpen = !this.imOpen
+    },
+    moveLeft() {
+      this.offset = -80
+    },
+    moveBack() {
+      this.offset = 0
+    },
+    handleMouseEnter() {
+      if (this.hideTimer) {
+        clearTimeout(this.hideTimer)
+        this.hideTimer = null
+      }
+      this.moveLeft()
+    },
+    handleMouseLeave() {
+      this.hideTimer = setTimeout(() => {
+        this.moveBack()
+        this.hideTimer = null
+      }, 5000)
+    },
     handleClickOutside() {
       this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
+    },
+    handleGlobalMouseDown(event) {
+      if (!this.imOpen) {
+        return
+      }
+      const panel = this.$refs.imPanel && this.$refs.imPanel.$el
+      if (panel && panel.contains(event.target)) {
+        return
+      }
+      const trigger = this.$el && this.$el.querySelector('.newIm')
+      if (trigger && trigger.contains(event.target)) {
+        return
+      }
+      this.imOpen = false
+    }
+  },
+  mounted() {
+    document.addEventListener('mousedown', this.handleGlobalMouseDown)
+  },
+  beforeDestroy() {
+    document.removeEventListener('mousedown', this.handleGlobalMouseDown)
+  },
+  data() {
+    return {
+      imOpen: false,
+      offset: 0,
+      hideTimer: null
     }
   }
 }
@@ -104,4 +173,51 @@ export default {
   .mobile .fixed-header {
     width: 100%;
   }
+
+  .newIm {
+    z-index: 101;
+    position: fixed;
+    bottom: 60px;
+    width: 70px;
+    height: 60px;
+    right: -50px;
+    border-radius: 50%;
+    box-shadow: 0 0 20px rgb(175, 175, 175);
+    transition: transform 0.3s ease, box-shadow 0.3s ease;
+    cursor: pointer;
+    background: #fff;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    .newImIcon {
+      width: 30px;
+      height: 30px;
+      color: #409EFF;
+    }
+
+    .num {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      border-radius: 50%;
+      width: 20px;
+      height: 20px;
+      text-align: center;
+      position: absolute;
+      top: 0;
+      right: 0;
+      color: #fff;
+      font-size: 12px;
+      background-color: #ff0000;
+    }
+  }
+
+  .im-box {
+    width: 80%;
+    z-index: 100;
+    position: fixed;
+    top: 100px;
+    right: 10px;
+  }
 </style>

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio