CommonHelper.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. <?php
  2. // app/Libraries/CommonHelper.php
  3. namespace App\Libraries;
  4. use Dcat\Admin\Admin;
  5. class CommonHelper
  6. {
  7. /*
  8. * $images 格式:['image.jpg','image2.jpg']
  9. * $ossSource 1:本地 2:相册
  10. * 返回显示的HTML显示图片
  11. */
  12. public static function displayImage($images,$boxSize=60,$imgSize=1024,$ossSource=1,$hasLink=true)
  13. {
  14. if (empty($images) || empty($images[0])) {
  15. $html = "";
  16. } else {
  17. // 默认显示 100x100 的图片
  18. $thumbnailImages = array_map(function ($imageUrl) use ($boxSize,$ossSource) {
  19. if ($ossSource == 1) {
  20. $imageUrl= CommonHelper::ossUrl($imageUrl);
  21. } else {
  22. $imageUrl= CommonHelper::albumUrl($imageUrl);
  23. }
  24. $extension = pathinfo($imageUrl, PATHINFO_EXTENSION);
  25. if ($extension == 'svg') {
  26. return $imageUrl;
  27. } else {
  28. return $imageUrl . "?x-oss-process=image/resize,m_pad,h_{$boxSize},w_{$boxSize},color_ffffff";
  29. }
  30. }, $images);
  31. // 生成点击查看大图的链接
  32. $largeImages = array_map(function ($imageUrl) use ($imgSize,$ossSource) {
  33. if ($ossSource == 1) {
  34. $imageUrl= CommonHelper::ossUrl($imageUrl);
  35. } else {
  36. $imageUrl= CommonHelper::albumUrl($imageUrl);
  37. }
  38. $extension = pathinfo($imageUrl, PATHINFO_EXTENSION);
  39. if ($extension == 'svg') {
  40. return $imageUrl;
  41. } else {
  42. return $imageUrl . "?x-oss-process=image/resize,m_lfit,w_{$imgSize},h_{$imgSize}";
  43. }
  44. }, $images);
  45. // 显示缩略图,并添加点击查看大图的功能
  46. $html = '';
  47. foreach ($thumbnailImages as $index => $thumbnailUrl) {
  48. $largeUrl = $largeImages[$index];
  49. if ($hasLink) {
  50. $html .= "<a href='$largeUrl' target='_blank'><img src='$thumbnailUrl' style='height:{$boxSize}px; margin-right:5px; border: 1px solid #ececf1;'></a>";
  51. } else {
  52. $html .= "<img src='$thumbnailUrl' style='height:{$boxSize}px; margin-right:5px; border: 1px solid #ececf1;'>";
  53. }
  54. }
  55. return $html;
  56. }
  57. return $html;
  58. }
  59. /*
  60. * 返回oss的url
  61. */
  62. public static function ossUrl($imageUrl)
  63. {
  64. if (strpos($imageUrl, 'http:') === 0 || strpos($imageUrl, 'https:') === 0) {
  65. return $imageUrl;
  66. }
  67. if (env('OSS_DOMAIN')) {
  68. return "http://".env('OSS_DOMAIN').'/'.$imageUrl;
  69. }
  70. return "http://".env('OSS_BUCKET').'.'.env('OSS_ENDPOINT').'/'.$imageUrl;
  71. }
  72. /*
  73. * 图库图片URL
  74. */
  75. public static function albumUrl($imageUrl)
  76. {
  77. if (strpos($imageUrl, 'http:') === 0 || strpos($imageUrl, 'https:') === 0) {
  78. return $imageUrl;
  79. }
  80. return env('ALBUM_OSS_URL').'/'.$imageUrl;
  81. }
  82. /*
  83. * 显示视频
  84. */
  85. public static function displayVideo($items,$videoCover,$videoSrc,$boxSize=150,$ossSource=1)
  86. {
  87. $html = '';
  88. if (is_array($items)) {
  89. foreach ($items as $item) {
  90. $item = (array) $item;
  91. $cover = $item[$videoCover];
  92. $src = $item[$videoSrc];
  93. if ($ossSource == 1) {
  94. $videoUrl = CommonHelper::ossUrl($src);
  95. } else {
  96. $videoUrl = CommonHelper::albumUrl($src);
  97. }
  98. if ($ossSource == 1) {
  99. $thumbnailUrl = CommonHelper::ossUrl($cover) . "?x-oss-process=image/resize,m_pad,h_{$boxSize},w_{$boxSize},color_ffffff";
  100. } else {
  101. $thumbnailUrl = CommonHelper::albumUrl($cover) . "?x-oss-process=image/resize,m_pad,h_{$boxSize},w_{$boxSize},color_ffffff";
  102. }
  103. $html .= '<div class="video-container"><img src="'.$thumbnailUrl.'" alt="Video Thumbnail" class="video-thumbnail"><a href="#" class="downloadVideo" videoUrl="'.$videoUrl.'")"><div class="video-download-button"></div></a> <a href="#" class="playVideo" videoUrl="'.$videoUrl.'")"><div class="play-button"></div></a></div>';
  104. }
  105. $html .= '<div class="video-popup" id="videoPopup"><span class="close-btn">&times;</span> <iframe src="" frameborder="0" allowfullscreen></iframe></div>';
  106. }
  107. //视频播放CSS
  108. Admin::style("
  109. .video-container {
  110. position: relative;
  111. display: inline-block;
  112. }
  113. .video-container img {
  114. height: 200px;
  115. margin-right: 5px;
  116. border: 1px solid #ececf1;
  117. }
  118. .video-download-button {
  119. position: absolute;
  120. top: 50%;
  121. left: 35%;
  122. transform: translate(-50%, -50%);
  123. width: 35px;
  124. height: 35px;
  125. background-color: rgba(0, 0, 0, 0.5);
  126. border-radius: 50%;
  127. cursor: pointer;
  128. }
  129. .video-download-button::after {
  130. content: '↓';
  131. font-size: 15px;
  132. color: white;
  133. position: absolute;
  134. top: 50%;
  135. left: 50%;
  136. transform: translate(-50%, -50%);
  137. }
  138. .play-button {
  139. position: absolute;
  140. top: 50%;
  141. left: 58%;
  142. transform: translate(-50%, -50%);
  143. width: 35px;
  144. height: 35px;
  145. background-color: rgba(0, 0, 0, 0.5);
  146. border-radius: 50%;
  147. cursor: pointer;
  148. }
  149. .play-button::after {
  150. content: '▶';
  151. font-size: 15px;
  152. color: white;
  153. position: absolute;
  154. top: 50%;
  155. left: 50%;
  156. transform: translate(-50%, -50%);
  157. }
  158. .video-popup {
  159. display: none;
  160. position: fixed;
  161. top: 50%;
  162. left: 50%;
  163. transform: translate(-50%, -50%);
  164. background-color: white;
  165. padding: 25px;
  166. box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
  167. z-index: 1000;
  168. }
  169. .video-popup iframe {
  170. width: 800px;
  171. height: 450px;
  172. }
  173. .close-btn {
  174. position: absolute;
  175. top: 0px;
  176. right: 5px;
  177. font-size: 24px;
  178. color: #000;
  179. cursor: pointer;
  180. }
  181. .close-btn:hover {
  182. color: #f00;
  183. }
  184. ");
  185. Admin::script("
  186. $('.playVideo').on('click', function(e) {
  187. e.preventDefault(); // 阻止默认跳转行为
  188. var videoUrl = $(this).attr('videoUrl'); // 获取 videoUrl 属性
  189. $('#videoPopup').css('display', 'block');
  190. $('#videoPopup iframe').attr('src', videoUrl); // 设置 iframe 的 src
  191. });
  192. $('.downloadVideo').on('click', function(e) {
  193. e.preventDefault();
  194. const button = $(this);
  195. const videoUrl = button.attr('videoUrl');
  196. // 使用 Dcat Admin 内置 loading 组件
  197. const loading = Dcat.loading('<i class=\"feather icon-download-cloud\"></i> downloading...');
  198. fetch(videoUrl, {
  199. headers: new Headers({
  200. 'Origin': location.origin,
  201. 'X-CSRF-TOKEN': Dcat.token // 集成 CSRF 保护[7](@ref)
  202. }),
  203. mode: 'cors'
  204. }).then(response => {
  205. if (!response.ok) throw new Error(`HTTP \${response.status}`);
  206. // 动态创建进度条组件
  207. const progress = new Dcat.Progress({
  208. title: '下载进度',
  209. showCancel: true,
  210. onCancel: () => controller.abort()
  211. });
  212. const reader = response.body.getReader();
  213. const controller = new AbortController();
  214. const contentLength = +response.headers.get('Content-Length');
  215. let receivedLength = 0;
  216. const chunks = [];
  217. // 流式处理优化
  218. const processChunk = ({ done, value }) => {
  219. if (done || progress.canceled) {
  220. if (!progress.canceled) {
  221. const blob = new Blob(chunks);
  222. const filename = videoUrl.split('/').pop() || 'video.mp4';
  223. const link = document.createElement('a');
  224. // 集成 Dcat 文件管理器[6](@ref)
  225. Dcat.handleFileDownload(blob, filename);
  226. }
  227. progress.close();
  228. loading.close();
  229. return;
  230. }
  231. chunks.push(value);
  232. receivedLength += value.length;
  233. progress.update({
  234. percent: Math.round((receivedLength / contentLength) * 100),
  235. statusText: `\${formatBytes(receivedLength)} / \${formatBytes(contentLength)}`
  236. });
  237. return reader.read().then(processChunk);
  238. };
  239. return reader.read().then(processChunk);
  240. }).catch(error => {
  241. loading.close();
  242. Dcat.error(`下载失败: \${error.message}`);
  243. console.error('Download Error:', error);
  244. });
  245. });
  246. // 字节单位转换工具函数
  247. function formatBytes(bytes) {
  248. const units = ['B', 'KB', 'MB', 'GB'];
  249. let i = 0;
  250. while (bytes >= 1024 && i < units.length - 1) {
  251. bytes /= 1024;
  252. i++;
  253. }
  254. return `\${bytes.toFixed(1)} \${units[i]}`;
  255. }
  256. // 点击关闭按钮关闭视频
  257. $('.close-btn').on('click', function() {
  258. $('#videoPopup').css('display', 'none');
  259. $('#videoPopup iframe').attr('src', ''); // 停止播放视频
  260. });
  261. ");
  262. return $html; // 当没有数组数据时
  263. }
  264. /*
  265. * 替换新增与编辑的url,在后边加上指定的参数 (适用于弹出框的新增与编辑)
  266. * $addButton = '.tree-quick-create';
  267. * $editButton = '.tree-quick-edit';
  268. * $paramsUrl = 'location=0';
  269. */
  270. public static function replaceAddEditerUrl($addButton,$editButton,$paramsUrl) {
  271. Admin::script(
  272. <<<JS
  273. var button = $('{$addButton}');
  274. var currentUrl = button.attr('data-url');
  275. if (currentUrl.indexOf('?') === -1) {
  276. button.attr('data-url', currentUrl + '?{$paramsUrl}');
  277. } else {
  278. button.attr('data-url', currentUrl + '&{$paramsUrl}');
  279. }
  280. $('{$editButton}').each(function() {
  281. var currentUrl = $(this).attr('data-url');
  282. if (currentUrl.indexOf('?') === -1) {
  283. $(this).attr('data-url', currentUrl + '?{$paramsUrl}');
  284. } else {
  285. // 如果已经有查询参数,添加 &id=123
  286. $(this).attr('data-url', currentUrl + '&{$paramsUrl}');
  287. }
  288. });
  289. JS
  290. );
  291. }
  292. public static function seoReplace($titleName = 'title',$modelName = 'pages')
  293. {
  294. if ($titleName) {
  295. Admin::script(
  296. <<<JS
  297. $('input[name="{$titleName}"]').on('input', function() {
  298. // 将 title 的值赋给 seo_title
  299. $('input[name="seo_title"]').val($(this).val());
  300. });
  301. JS
  302. );
  303. }
  304. //ajax 生成slug
  305. if ($modelName) {
  306. Admin::script(
  307. <<<JS
  308. $('input[name="{$titleName}"]').on('blur', function() {
  309. //通过ajax请求修改slug
  310. title = $(this).val();
  311. $.ajax({
  312. url: '/dist/api/generate-slug',
  313. type: 'GET',
  314. data: {
  315. model:'{$modelName}',
  316. title: title,
  317. },
  318. success: function(response) {
  319. $('input[name="slug"]').val(response.slug);
  320. }
  321. });
  322. });
  323. JS
  324. );
  325. }
  326. }
  327. public static function slug_fix($slug)
  328. {
  329. if ($slug == '') {
  330. return '';
  331. } else {
  332. $slug = preg_replace('/\s+/', '-', $slug);
  333. $slug = strtolower($slug);
  334. return $slug;
  335. }
  336. }
  337. /*
  338. * 用于详情页,图片下载与放大
  339. */
  340. public static function viewDownloadEnlarge() {
  341. //css
  342. Admin::style(
  343. <<<CSS
  344. .download-wrapper::after {
  345. content: "";
  346. position: absolute;
  347. top: 0;
  348. left: 0;
  349. width: 100%;
  350. height: 100%;
  351. background: rgba(58, 54, 54, 0.34); /* 浅灰色半透明 */
  352. opacity: 0;
  353. transition: opacity 0.3s ease;
  354. pointer-events: none;
  355. border-radius: 8px;
  356. }
  357. .download-wrapper {
  358. position: relative;
  359. display: inline-block;
  360. margin: 15px;
  361. overflow: hidden;
  362. border-radius: 8px;
  363. transition: transform 0.3s ease;
  364. }
  365. .download-hover {
  366. position: absolute;
  367. top: 50%;
  368. left: 50%;
  369. transform: translate(-50%, -50%);
  370. opacity: 0;
  371. transition: opacity 0.3s ease;
  372. display: flex;
  373. gap: 12px;
  374. z-index: 1; /* 确保按钮在遮罩层上方 */
  375. }
  376. .download-btn {
  377. background: rgba(255, 255, 255, 0.95);
  378. border: none;
  379. width: 42px;
  380. height: 42px;
  381. border-radius: 50%;
  382. cursor: pointer;
  383. box-shadow: 0 3px 6px rgba(0,0,0,0.16);
  384. transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
  385. }
  386. .download-btn.disabled {
  387. background: rgba(189, 189, 189, 0.95);
  388. cursor: not-allowed;
  389. }
  390. .download-btn i {
  391. font-size: 18px;
  392. color: #2196F3;
  393. }
  394. .download-wrapper:hover::after {
  395. opacity: 1;
  396. }
  397. .download-wrapper:hover .download-hover {
  398. opacity: 1;
  399. }
  400. .auto-download-image {
  401. display: block;
  402. transition: transform 0.3s ease;
  403. max-width: 100%;
  404. height: auto;
  405. border-radius: 8px;
  406. }
  407. .download-wrapper:hover img {
  408. transform: scale(1.04);
  409. }
  410. /* 新增全屏样式 */
  411. .fullscreen-overlay {
  412. position: fixed;
  413. top: 0;
  414. left: 0;
  415. width: 100%;
  416. height: 100%;
  417. background: rgba(0,0,0,0.95);
  418. display: none;
  419. z-index: 1000;
  420. cursor: zoom-out;
  421. }
  422. .fullscreen-image {
  423. position: absolute;
  424. max-width: 90%;
  425. max-height: 90%;
  426. top: 50%;
  427. left: 50%;
  428. transform: translate(-50%, -50%);
  429. border-radius: 8px;
  430. animation: zoomIn 0.3s ease;
  431. }
  432. @keyframes zoomIn {
  433. from { transform: translate(-50%, -50%) scale(0.8); }
  434. to { transform: translate(-50%, -50%) scale(1); }
  435. }
  436. .download-wrapper:hover .download-hover {
  437. opacity: 1;
  438. }
  439. CSS );
  440. //js
  441. Admin::script(
  442. <<<JS
  443. const fullscreenOverlay = $('.fullscreen-overlay');
  444. const fullscreenImage = $('.fullscreen-image');
  445. function initDownloadButtons() {
  446. $('.box-body').find("img").each(function() {
  447. // 新增判断:如果父元素是<a>或已有download-wrapper则跳过
  448. if ($(this).parent().hasClass('download-wrapper') || $(this).parent().is('a')) return;
  449. if ($(this).hasClass('fullscreen-image') || $(this).hasClass('video-thumbnail')) {
  450. return;
  451. }
  452. const fileName = this.src.split('/').pop().split('?')[0];
  453. $(this).wrap('<div class="download-wrapper"></div>')
  454. .after(`<div class="download-hover">
  455. <button class="download-btn" title="下载">
  456. <i class="feather icon-download"></i>
  457. </button>
  458. <button class="download-btn zoom-btn" title="放大">
  459. <i class="feather icon-zoom-in"></i>
  460. </button>
  461. </div>`);
  462. const \$btn = $(this).parent().find('.download-btn').first();
  463. \$btn.click(function() {
  464. const btn = $(this);
  465. btn.addClass('disabled');
  466. downloadImage(
  467. btn.closest('.download-wrapper').find('img')[0].src,
  468. fileName
  469. ).finally(() => btn.removeClass('disabled'));
  470. });
  471. // 全屏放大功能
  472. $(this).next().find('.zoom-btn').click(function() {
  473. let imgSrc = $(this).closest('.download-wrapper').find('img').attr('src');
  474. imgSrc = imgSrc.replace(/\?.*$/, '');
  475. fullscreenImage.attr('src', imgSrc);
  476. fullscreenOverlay.fadeIn(200);
  477. });
  478. });
  479. }
  480. // 关闭全屏
  481. fullscreenOverlay.click(function(e) {
  482. if (e.target === this) $(this).fadeOut(200);
  483. });
  484. function downloadImage(url, filename) {
  485. url = url.replace(/\?.*$/, '');
  486. return new Promise((resolve, reject) => {
  487. fetch(url)
  488. .then(response => {
  489. if (!response.ok) throw new Error('网络异常');
  490. return response.blob()
  491. })
  492. .then(blob => {
  493. const blobUrl = URL.createObjectURL(blob);
  494. console.log(blobUrl);
  495. const link = document.createElement('a');
  496. link.href = blobUrl;
  497. link.download = filename || 'image.jpg';
  498. document.body.appendChild(link);
  499. link.click();
  500. setTimeout(() => {
  501. document.body.removeChild(link);
  502. URL.revokeObjectURL(blobUrl);
  503. }, 100);
  504. resolve();
  505. })
  506. .catch(error => {
  507. console.error(error);
  508. alert('下载失败');
  509. reject(error);
  510. });
  511. });
  512. }
  513. initDownloadButtons();
  514. JS);
  515. }
  516. /*
  517. * 用于详情页,图片下载与放大 HTML
  518. */
  519. public static function viewDownloadEnlargeHtml() {
  520. return '<div class="fullscreen-overlay"><img src="" class="fullscreen-image"></div>';
  521. }
  522. }