You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

401 lines
14 KiB

6 months ago
5 months ago
6 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
  1. import Vue from 'vue';
  2. import store from '@/store'
  3. // v-dialogDrag: 弹窗拖拽属性
  4. Vue.directive('drag', {
  5. bind(el, binding, vnode, oldVnode) {
  6. const dialogHeaderEl = el.querySelector('.el-dialog__header');
  7. const dragDom = el.querySelector('.el-dialog');
  8. dialogHeaderEl.style.cssText += ';cursor:move;'
  9. dragDom.style.cssText += ';top:0px;'
  10. // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
  11. const sty = (() => {
  12. if (window.document.currentStyle) {
  13. return (dom, attr) => dom.currentStyle[attr];
  14. } else {
  15. return (dom, attr) => getComputedStyle(dom, false)[attr];
  16. }
  17. })()
  18. dialogHeaderEl.onmousedown = (e) => {
  19. // 鼠标按下,计算当前元素距离可视区的距离
  20. const disX = e.clientX - dialogHeaderEl.offsetLeft;
  21. const disY = e.clientY - dialogHeaderEl.offsetTop;
  22. const screenWidth = document.body.clientWidth; // body当前宽度
  23. const screenHeight = document.documentElement.clientHeight; // 可见区域高度(应为body高度,可某些环境下无法获取)
  24. const dragDomWidth = dragDom.offsetWidth; // 对话框宽度
  25. const dragDomheight = dragDom.offsetHeight; // 对话框高度
  26. const minDragDomLeft = dragDom.offsetLeft;
  27. const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth;
  28. const minDragDomTop = dragDom.offsetTop;
  29. const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomheight;
  30. // 获取到的值带px 正则匹配替换
  31. let styL = sty(dragDom, 'left');
  32. let styT = sty(dragDom, 'top');
  33. // 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
  34. if (styL.includes('%')) {
  35. styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100);
  36. styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100);
  37. } else {
  38. styL = +styL.replace(/\px/g, '');
  39. styT = +styT.replace(/\px/g, '');
  40. };
  41. document.onmousemove = function (e) {
  42. // 通过事件委托,计算移动的距离
  43. let left = e.clientX - disX;
  44. let top = e.clientY - disY;
  45. // 边界处理
  46. if (-(left) > minDragDomLeft) {
  47. left = -(minDragDomLeft);
  48. } else if (left > maxDragDomLeft) {
  49. left = maxDragDomLeft;
  50. }
  51. if (-(top) > minDragDomTop) {
  52. top = -(minDragDomTop);
  53. } else if (top > maxDragDomTop) {
  54. top = maxDragDomTop;
  55. }
  56. // 移动当前元素
  57. dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`;
  58. };
  59. document.onmouseup = function (e) {
  60. document.onmousemove = null;
  61. document.onmouseup = null;
  62. };
  63. }
  64. }
  65. })
  66. //---------------以下都可删---------------------
  67. // v-fireworks: 容器内按钮点击烟花特效(全局可复用)
  68. Vue.directive('fireworks', {
  69. bind(el, binding) {
  70. const colors = ['#FFD700', '#DAA520', '#B8860B', '#C9A227', '#E6B800'];
  71. const getSkyConfig = () => {
  72. let type = 'rain';
  73. let enabled = true;
  74. const val = binding && binding.value;
  75. if (typeof val === 'string') {
  76. if (val === 'rain' || val === 'meteor') type = val;
  77. if (val === 'none') enabled = false;
  78. } else if (typeof val === 'object' && val) {
  79. if (val.sky === 'rain' || val.sky === 'meteor') type = val.sky;
  80. if (val.sky === 'none') enabled = false;
  81. if (typeof val.skyEnabled === 'boolean') enabled = val.skyEnabled;
  82. if (typeof val.enabled === 'boolean') enabled = val.enabled;
  83. } else if (val === false) {
  84. enabled = false;
  85. }
  86. const ds = el.getAttribute && el.getAttribute('data-sky');
  87. if (ds === 'rain' || ds === 'meteor') type = ds;
  88. if (ds === 'none') enabled = false;
  89. const dse = el.getAttribute && el.getAttribute('data-sky-enabled');
  90. if (dse === 'false' || dse === '0') enabled = false;
  91. if (dse === 'true' || dse === '1') enabled = true;
  92. return { type, enabled };
  93. }
  94. // ===== 全屏天空特效(雨/流星) =====
  95. let skyCanvas = null;
  96. let skyCtx = null;
  97. let skyRaf = null;
  98. let skyTimer = null;
  99. let skyDelayTimer = null;
  100. let skyParticles = [];
  101. const ensureSkyCanvas = () => {
  102. if (skyCanvas) return;
  103. skyCanvas = document.createElement('canvas');
  104. skyCanvas.className = 'sky-effect-canvas';
  105. skyCanvas.style.position = 'fixed';
  106. skyCanvas.style.left = '0';
  107. skyCanvas.style.top = '0';
  108. skyCanvas.style.width = '100vw';
  109. skyCanvas.style.height = '100vh';
  110. skyCanvas.style.pointerEvents = 'none';
  111. skyCanvas.style.zIndex = '99998';
  112. document.body.appendChild(skyCanvas);
  113. skyCtx = skyCanvas.getContext('2d');
  114. const resize = () => {
  115. skyCanvas.width = window.innerWidth;
  116. skyCanvas.height = window.innerHeight;
  117. };
  118. resize();
  119. window.addEventListener('resize', resize);
  120. skyCanvas.__resize__ = resize;
  121. };
  122. const clearSky = () => {
  123. if (skyRaf) cancelAnimationFrame(skyRaf);
  124. skyRaf = null;
  125. if (skyTimer) clearTimeout(skyTimer);
  126. skyTimer = null;
  127. if (skyDelayTimer) clearTimeout(skyDelayTimer);
  128. skyDelayTimer = null;
  129. if (skyCanvas) {
  130. window.removeEventListener('resize', skyCanvas.__resize__);
  131. if (skyCanvas.parentNode) skyCanvas.parentNode.removeChild(skyCanvas);
  132. }
  133. skyCanvas = null;
  134. skyCtx = null;
  135. skyParticles = [];
  136. };
  137. const startRain = (duration = 3000) => {
  138. ensureSkyCanvas();
  139. const count = Math.min(140, Math.floor((window.innerWidth * window.innerHeight) / 12000));
  140. skyParticles = new Array(count).fill(0).map(() => ({
  141. x: Math.random() * skyCanvas.width,
  142. y: Math.random() * skyCanvas.height,
  143. len: 10 + Math.random() * 14,
  144. speed: 3 + Math.random() * 6,
  145. wind: 1 + Math.random() * 2
  146. }));
  147. const stroke = 'rgba(255, 215, 0, 0.8)';
  148. const draw = () => {
  149. if (!skyCtx) return;
  150. skyCtx.clearRect(0, 0, skyCanvas.width, skyCanvas.height);
  151. skyCtx.strokeStyle = stroke;
  152. skyCtx.lineWidth = 1.2;
  153. skyCtx.lineCap = 'round';
  154. for (const p of skyParticles) {
  155. skyCtx.beginPath();
  156. skyCtx.moveTo(p.x, p.y);
  157. skyCtx.lineTo(p.x + p.wind, p.y + p.len);
  158. skyCtx.stroke();
  159. p.x += p.wind;
  160. p.y += p.speed;
  161. if (p.y > skyCanvas.height || p.x > skyCanvas.width) {
  162. p.x = Math.random() * skyCanvas.width;
  163. p.y = -20;
  164. }
  165. }
  166. skyRaf = requestAnimationFrame(draw);
  167. };
  168. draw();
  169. skyTimer = setTimeout(clearSky, duration);
  170. };
  171. const startMeteor = (duration = 3000) => {
  172. ensureSkyCanvas();
  173. const count = 8;
  174. skyParticles = new Array(count).fill(0).map(() => newMeteor());
  175. function newMeteor() {
  176. const startX = Math.random() * skyCanvas.width;
  177. const startY = Math.random() * (skyCanvas.height * 0.3);
  178. const speed = 6 + Math.random() * 6;
  179. const angle = Math.PI / 3; // 60° 斜向
  180. const vx = Math.cos(angle) * speed;
  181. const vy = Math.sin(angle) * speed;
  182. const len = 120 + Math.random() * 120;
  183. const life = 40 + Math.random() * 40;
  184. return { x: startX, y: startY, vx, vy, len, life, maxLife: life };
  185. }
  186. const draw = () => {
  187. if (!skyCtx) return;
  188. skyCtx.clearRect(0, 0, skyCanvas.width, skyCanvas.height);
  189. for (let i = 0; i < skyParticles.length; i++) {
  190. const m = skyParticles[i];
  191. const grad = skyCtx.createLinearGradient(m.x, m.y, m.x - m.vx * 10, m.y - m.vy * 10);
  192. grad.addColorStop(0, 'rgba(255,215,0,0.9)');
  193. grad.addColorStop(1, 'rgba(255,215,0,0.0)');
  194. skyCtx.strokeStyle = grad;
  195. skyCtx.lineWidth = 2;
  196. skyCtx.beginPath();
  197. skyCtx.moveTo(m.x, m.y);
  198. skyCtx.lineTo(m.x - (m.vx * (m.len / 10)), m.y - (m.vy * (m.len / 10)));
  199. skyCtx.stroke();
  200. m.x += m.vx;
  201. m.y += m.vy;
  202. m.life -= 1;
  203. if (m.life <= 0 || m.x > skyCanvas.width + 100 || m.y > skyCanvas.height + 100) {
  204. skyParticles[i] = newMeteor();
  205. }
  206. }
  207. skyRaf = requestAnimationFrame(draw);
  208. };
  209. draw();
  210. skyTimer = setTimeout(clearSky, duration);
  211. };
  212. const startSkyEffect = () => {
  213. const cfg = getSkyConfig();
  214. if (!cfg.enabled) return;
  215. // 若已有画布或定时器,刷新计时即可,避免叠加
  216. if (skyTimer) {
  217. clearTimeout(skyTimer);
  218. skyTimer = null;
  219. }
  220. if (skyDelayTimer) {
  221. clearTimeout(skyDelayTimer);
  222. skyDelayTimer = null;
  223. }
  224. if (cfg.type === 'meteor') {
  225. startMeteor(3500);
  226. } else {
  227. // 雨滴延迟1秒出现,持续时间为1秒
  228. skyDelayTimer = setTimeout(() => {
  229. startRain(1000);
  230. }, 1000);
  231. }
  232. };
  233. const isButtonLike = (node) => {
  234. if (!node) return false;
  235. if (node.closest) {
  236. const match = node.closest('button, .el-button, [role="button"], input[type="button"], input[type="submit"], a.el-button, a');
  237. return !!match;
  238. }
  239. return false;
  240. }
  241. const spawnFireworks = (x, y) => {
  242. const count = 14;
  243. for (let i = 0; i < count; i++) {
  244. const particle = document.createElement('span');
  245. particle.className = 'firework-particle';
  246. const angle = (Math.PI * 2 * i) / count + Math.random() * 0.3;
  247. const distance = 60 + Math.random() * 60;
  248. const dx = Math.cos(angle) * distance;
  249. const dy = Math.sin(angle) * distance;
  250. particle.style.left = x + 'px';
  251. particle.style.top = y + 'px';
  252. particle.style.background = colors[i % colors.length];
  253. particle.style.setProperty('--dx', dx + 'px');
  254. particle.style.setProperty('--dy', dy + 'px');
  255. document.body.appendChild(particle);
  256. setTimeout(() => {
  257. if (particle && particle.parentNode) {
  258. particle.parentNode.removeChild(particle);
  259. }
  260. }, 700);
  261. }
  262. };
  263. const showOverlayImage = (x, y) => {
  264. const img = document.createElement('img');
  265. img.className = 'click-overlay-image';
  266. img.src = '/static/img/保密资源.png';
  267. img.style.left = x + 'px';
  268. img.style.top = y + 'px';
  269. document.body.appendChild(img);
  270. setTimeout(() => {
  271. if (img && img.parentNode) {
  272. img.parentNode.removeChild(img);
  273. }
  274. }, 1000);
  275. };
  276. const handler = (e) => {
  277. if (store.state.common.effectsEnabled==='N') return;
  278. if (!isButtonLike(e.target)) return;
  279. const x = e.clientX;
  280. const y = e.clientY;
  281. spawnFireworks(x, y);
  282. startSkyEffect();
  283. showOverlayImage(x, y);
  284. };
  285. el.__fireworksHandler__ = handler;
  286. el.addEventListener('click', handler, false);
  287. },
  288. unbind(el) {
  289. if (el.__fireworksHandler__) {
  290. el.removeEventListener('click', el.__fireworksHandler__, false);
  291. delete el.__fireworksHandler__;
  292. }
  293. }
  294. })
  295. // v-click-image: 点击显示图片浮层(不拦截交互,可全局复用)
  296. Vue.directive('click-image', {
  297. bind(el, binding) {
  298. const defaults = {
  299. src: '/static/img/保密资源.png',
  300. duration: 1000,
  301. width: '90px',
  302. height: '90px',
  303. any: false // false: 仅按钮/a 元素触发;true: 任意元素点击触发
  304. };
  305. const resolveConfig = () => {
  306. const val = binding && binding.value;
  307. const cfg = { ...defaults };
  308. if (typeof val === 'string') {
  309. cfg.src = val || cfg.src;
  310. } else if (val && typeof val === 'object') {
  311. if (val.src) cfg.src = val.src;
  312. if (val.duration != null) cfg.duration = Number(val.duration) || cfg.duration;
  313. if (val.width) cfg.width = typeof val.width === 'number' ? (val.width + 'px') : val.width;
  314. if (val.height) cfg.height = typeof val.height === 'number' ? (val.height + 'px') : val.height;
  315. if (typeof val.any === 'boolean') cfg.any = val.any;
  316. }
  317. // data-* 兜底
  318. const ds = el.getAttribute && el.getAttribute('data-click-image');
  319. if (ds) cfg.src = ds;
  320. const dsd = el.getAttribute && el.getAttribute('data-click-image-duration');
  321. if (dsd) cfg.duration = Number(dsd) || cfg.duration;
  322. const dsw = el.getAttribute && el.getAttribute('data-click-image-width');
  323. if (dsw) cfg.width = dsw.match(/^\d+$/) ? (dsw + 'px') : dsw;
  324. const dsh = el.getAttribute && el.getAttribute('data-click-image-height');
  325. if (dsh) cfg.height = dsh.match(/^\d+$/) ? (dsh + 'px') : dsh;
  326. const dsa = el.getAttribute && el.getAttribute('data-click-image-any');
  327. if (dsa === 'true' || dsa === '1') cfg.any = true;
  328. if (dsa === 'false' || dsa === '0') cfg.any = false;
  329. return cfg;
  330. };
  331. const isButtonLike = (node) => {
  332. if (!node) return false;
  333. if (node.closest) {
  334. const match = node.closest('button, .el-button, [role="button"], input[type="button"], input[type="submit"], a.el-button, a');
  335. return !!match;
  336. }
  337. return false;
  338. };
  339. const show = (x, y, cfg) => {
  340. const img = document.createElement('img');
  341. img.className = 'click-overlay-image';
  342. img.src = cfg.src;
  343. img.style.left = x + 'px';
  344. img.style.top = y + 'px';
  345. if (cfg.width) img.style.width = cfg.width;
  346. if (cfg.height) img.style.height = cfg.height;
  347. document.body.appendChild(img);
  348. setTimeout(() => {
  349. if (img && img.parentNode) img.parentNode.removeChild(img);
  350. }, cfg.duration);
  351. };
  352. const handler = (e) => {
  353. if (store.state.common.effectsEnabled==='N') return;
  354. const cfg = resolveConfig();
  355. if (!cfg.any && !isButtonLike(e.target)) return;
  356. const x = e.clientX;
  357. const y = e.clientY;
  358. show(x, y, cfg);
  359. };
  360. el.__clickImageHandler__ = handler;
  361. el.addEventListener('click', handler, false);
  362. },
  363. unbind(el) {
  364. if (el.__clickImageHandler__) {
  365. el.removeEventListener('click', el.__clickImageHandler__, false);
  366. delete el.__clickImageHandler__;
  367. }
  368. }
  369. });