message.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. import { usezIndex } from "./vue/hooks";
  2. /**
  3. * 消息提醒功能
  4. * @param params
  5. */
  6. function useMessage(params = {}) {
  7. const doc = document;
  8. const cssModule = `__${Math.random().toString(36).slice(2, 7)}`;
  9. const className = {
  10. box: `msg-box${cssModule}`,
  11. hide: `hide${cssModule}`,
  12. text: `msg-text${cssModule}`,
  13. icon: `msg-icon${cssModule}`
  14. };
  15. const style = doc.createElement("style");
  16. style.textContent = `
  17. .${className.box}, .${className.icon}, .${className.text} {
  18. padding: 0;
  19. margin: 0;
  20. box-sizing: border-box;
  21. }
  22. .${className.box} {
  23. position: fixed;
  24. top: 0;
  25. left: 50%;
  26. display: flex;
  27. padding: 12px 16px;
  28. border-radius: 2px;
  29. background-color: #fff;
  30. box-shadow: 0 3px 3px -2px rgba(0,0,0,.2),0 3px 4px 0 rgba(0,0,0,.14),0 1px 8px 0 rgba(0,0,0,.12);
  31. white-space: nowrap;
  32. animation: ${className.box}-move .4s;
  33. transition: .4s all;
  34. transform: translate3d(-50%, 0%, 0);
  35. opacity: 1;
  36. overflow: hidden;
  37. }
  38. .${className.box}::after {
  39. content: "";
  40. position: absolute;
  41. left: 0;
  42. top: 0;
  43. height: 100%;
  44. width: 4px;
  45. }
  46. @keyframes ${className.box}-move {
  47. 0% {
  48. opacity: 0;
  49. transform: translate3d(-50%, -100%, 0);
  50. }
  51. 100% {
  52. opacity: 1;
  53. transform: translate3d(-50%, 0%, 0);
  54. }
  55. }
  56. .${className.box}.${className.hide} {
  57. opacity: 0;
  58. /* transform: translate3d(-50%, -100%, 0); */
  59. transform: translate3d(-50%, -100%, 0) scale(0);
  60. }
  61. .${className.icon} {
  62. display: inline-block;
  63. width: 18px;
  64. height: 18px;
  65. border-radius: 50%;
  66. overflow: hidden;
  67. margin-right: 6px;
  68. position: relative;
  69. }
  70. .${className.text} {
  71. font-size: 14px;
  72. line-height: 18px;
  73. color: #555;
  74. }
  75. .${className.icon}::after,
  76. .${className.icon}::before {
  77. position: absolute;
  78. content: "";
  79. background-color: #fff;
  80. }
  81. .${className.box}.info .${className.icon}, .${className.box}.info::after {
  82. background-color: #1890ff;
  83. }
  84. .${className.box}.success .${className.icon}, .${className.box}.success::after {
  85. background-color: #52c41a;
  86. }
  87. .${className.box}.warning .${className.icon}, .${className.box}.warning::after {
  88. background-color: #faad14;
  89. }
  90. .${className.box}.error .${className.icon}, .${className.box}.error::after {
  91. background-color: #ff4d4f;
  92. }
  93. .${className.box}.info .${className.icon}::after,
  94. .${className.box}.warning .${className.icon}::after {
  95. top: 15%;
  96. left: 50%;
  97. margin-left: -1px;
  98. width: 2px;
  99. height: 2px;
  100. border-radius: 50%;
  101. }
  102. .${className.box}.info .${className.icon}::before,
  103. .${className.box}.warning .${className.icon}::before {
  104. top: calc(15% + 4px);
  105. left: 50%;
  106. margin-left: -1px;
  107. width: 2px;
  108. height: 40%;
  109. }
  110. .${className.box}.error .${className.icon}::after,
  111. .${className.box}.error .${className.icon}::before {
  112. top: 20%;
  113. left: 50%;
  114. width: 2px;
  115. height: 60%;
  116. margin-left: -1px;
  117. border-radius: 1px;
  118. }
  119. .${className.box}.error .${className.icon}::after {
  120. transform: rotate(-45deg);
  121. }
  122. .${className.box}.error .${className.icon}::before {
  123. transform: rotate(45deg);
  124. }
  125. .${className.box}.success .${className.icon}::after {
  126. box-sizing: content-box;
  127. background-color: transparent;
  128. border: 2px solid #fff;
  129. border-left: 0;
  130. border-top: 0;
  131. height: 50%;
  132. left: 35%;
  133. top: 13%;
  134. transform: rotate(45deg);
  135. width: 20%;
  136. transform-origin: center;
  137. }
  138. `.replace(/(\n|\t|\s)*/ig, "$1").replace(/\n|\t|\s(\{|\}|\,|\:|\;)/ig, "$1").replace(/(\{|\}|\,|\:|\;)\s/ig, "$1");
  139. doc.head.appendChild(style);
  140. /** 消息队列 */
  141. const messageList = [];
  142. /**
  143. * 获取指定`item`的定位`top`
  144. * @param el
  145. */
  146. function getItemTop(el) {
  147. let top = 10;
  148. for (let i = 0; i < messageList.length; i++) {
  149. const item = messageList[i];
  150. if (el && el === item) {
  151. break;
  152. }
  153. top += item.clientHeight + 20;
  154. }
  155. return top;
  156. }
  157. /**
  158. * 删除指定列表项
  159. * @param el
  160. */
  161. function removeItem(el) {
  162. for (let i = 0; i < messageList.length; i++) {
  163. const item = messageList[i];
  164. if (item === el) {
  165. messageList.splice(i, 1);
  166. break;
  167. }
  168. }
  169. el.classList.add(className.hide);
  170. messageList.forEach(function (item) {
  171. item.style.top = `${getItemTop(item)}px`;
  172. });
  173. }
  174. /**
  175. * 显示一条消息
  176. * @param content 内容
  177. * @param type 消息类型
  178. * @param duration 持续时间,优先级比默认值高
  179. */
  180. function show(content, type = "info", duration) {
  181. const el = doc.createElement("div");
  182. el.className = `${className.box} ${type}`;
  183. el.style.top = `${getItemTop()}px`;
  184. el.style.zIndex = zIndex.message;
  185. el.innerHTML = `
  186. <span class="${className.icon}"></span>
  187. <span class="${className.text}">${content}</span>
  188. `;
  189. messageList.push(el);
  190. doc.body.appendChild(el);
  191. // 添加动画监听事件
  192. function animationEnd() {
  193. el.removeEventListener("animationend", animationEnd);
  194. setTimeout(removeItem, duration || params.duration || 3000, el);
  195. }
  196. el.addEventListener("animationend", animationEnd);
  197. function transitionEnd() {
  198. if (getComputedStyle(el).opacity !== "0")
  199. return;
  200. el.removeEventListener("transitionend", transitionEnd);
  201. el.remove();
  202. }
  203. el.addEventListener("transitionend", transitionEnd);
  204. }
  205. return {
  206. show,
  207. /** 普通描述提示 */
  208. info(msg) {
  209. show(msg, "info");
  210. },
  211. /** 成功提示 */
  212. success(msg) {
  213. show(msg, "success");
  214. },
  215. /** 警告提示 */
  216. warning(msg) {
  217. show(msg, "warning");
  218. },
  219. /** 错误提示 */
  220. error(msg) {
  221. show(msg, "error");
  222. }
  223. };
  224. }
  225. /** 对话框控件 */
  226. function useDialog() {
  227. const doc = document;
  228. const cssModule = `__${Math.random().toString(36).slice(2, 7)}`;
  229. const className = {
  230. mask: `dialog-mask${cssModule}`,
  231. popup: `dialog-popup${cssModule}`,
  232. title: `dialog-title${cssModule}`,
  233. content: `dialog-content${cssModule}`,
  234. footer: `dialog-footer${cssModule}`,
  235. confirm: `confirm${cssModule}`,
  236. fade: `fade${cssModule}`,
  237. show: `show${cssModule}`,
  238. hide: `hide${cssModule}`
  239. };
  240. const cssText = `
  241. .${className.mask} {
  242. --time: .3s;
  243. --transition: .3s all;
  244. --black: #333;
  245. --text-color: #555;
  246. --confirm-bg: #2ec1cb;
  247. position: fixed;
  248. top: 0;
  249. left: 0;
  250. width: 100%;
  251. height: 100%;
  252. background-color: rgba(0,0,0,0.45);
  253. display: flex;
  254. align-items: center;
  255. justify-content: center;
  256. transition: var(--transition);
  257. animation: ${className.fade} var(--time);
  258. }
  259. .${className.mask} * {
  260. margin: 0;
  261. padding: 0;
  262. box-sizing: border-box;
  263. }
  264. .${className.popup} {
  265. width: 74%;
  266. max-width: 375px;
  267. border-radius: var(--border-radius);
  268. box-shadow: 0px 1px 5px 0px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 3px 1px -2px rgba(0, 0, 0, 0.12);
  269. background-color: #fff;
  270. transition: var(--transition);
  271. animation: ${className.show} var(--time);
  272. }
  273. .${className.title} {
  274. font-size: 18px;
  275. padding: 12px 15px;
  276. border-bottom: solid 1px #eee;
  277. font-weight: normal;
  278. color: var(--black);
  279. text-align: left;
  280. }
  281. .${className.content} {
  282. padding: 16px 15px;
  283. font-size: 15px;
  284. color: var(--text-color);
  285. text-align: left;
  286. }
  287. .${className.footer} {
  288. width: 100%;
  289. text-align: right;
  290. border-top: solid 1px #eee;
  291. padding: 12px 15px;
  292. }
  293. @keyframes ${className.fade} {
  294. 0% { opacity: 0; }
  295. 100% { opacity: 1; }
  296. }
  297. @keyframes ${className.show} {
  298. 0% { transform: translate3d(var(--x), var(--y), 0) scale(0); }
  299. 100% { transform: translate3d(0, 0, 0) scale(1); }
  300. }
  301. .${className.mask}.${className.hide} {
  302. opacity: 0;
  303. }
  304. .${className.mask}.${className.hide} .${className.popup} {
  305. transform: translate3d(var(--x), var(--y), 0) scale(0);
  306. }
  307. `;
  308. const style = doc.createElement("style");
  309. style.textContent = cssText.replace(/(\n|\t|\s)*/ig, "$1").replace(/\n|\t|\s(\{|\}|\,|\:|\;)/ig, "$1").replace(/(\{|\}|\,|\:|\;)\s/ig, "$1");
  310. doc.head.appendChild(style);
  311. /** 点击记录坐标 */
  312. const clickSize = {
  313. x: "0vw",
  314. y: "0vh"
  315. };
  316. // 添加点击事件,并记录每次点击坐标
  317. doc.addEventListener("click", function (e) {
  318. const { innerWidth, innerHeight } = window;
  319. const centerX = innerWidth / 2;
  320. const centerY = innerHeight / 2;
  321. const pageY = e.clientY - centerY;
  322. const pageX = e.clientX - centerX;
  323. clickSize.x = `${pageX / innerWidth * 100}vw`;
  324. clickSize.y = `${pageY / innerHeight * 100}vh`;
  325. }, true);
  326. /**
  327. * 输出节点
  328. * @param option
  329. */
  330. function show(option) {
  331. const el = doc.createElement("section");
  332. el.className = className.mask;
  333. el.style.zIndex = zIndex.dialog;
  334. // 设置起始偏移位置
  335. el.style.setProperty("--x", clickSize.x);
  336. el.style.setProperty("--y", clickSize.y);
  337. // 设置完之后还原坐标位置
  338. clickSize.x = "0vw";
  339. clickSize.y = "0vh";
  340. const cancelBtn = option.cancelText ? `<button class="the-btn">${option.cancelText}</button>` : "";
  341. el.innerHTML = `
  342. <div class="${className.popup}">
  343. <h2 class="${className.title}">${typeof option.title === "string" ? option.title : "提示"}</h2>
  344. <div class="${className.content}">${option.content}</div>
  345. <div class="${className.footer}">
  346. ${cancelBtn}
  347. <button class="${className.confirm} the-btn blue">${option.confirmText || "确认"}</button>
  348. </div>
  349. </div>
  350. `;
  351. doc.body.appendChild(el);
  352. el.addEventListener("transitionend", function (e) {
  353. e.target === el && el.classList.contains(className.hide) && el.remove();
  354. });
  355. function hide() {
  356. el.classList.add(className.hide);
  357. }
  358. if (option.cancelText) {
  359. el.querySelector(`.${className.footer} button`).onclick = function () {
  360. hide();
  361. option.cancel && option.cancel();
  362. };
  363. }
  364. el.querySelector(`.${className.confirm}`).onclick = function () {
  365. hide();
  366. option.confirm && option.confirm();
  367. };
  368. }
  369. return {
  370. show
  371. };
  372. }
  373. const zIndex = {
  374. get message() {
  375. return (usezIndex() + 20).toString();
  376. },
  377. get dialog() {
  378. return (usezIndex() + 10).toString();
  379. }
  380. };
  381. /** 顶部消息提醒控件 */
  382. export const message = useMessage({
  383. duration: 3600
  384. });
  385. const dialog = useDialog();
  386. /** 对话弹框控件 */
  387. export const messageBox = dialog.show;