common.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. /**
  2. * ProxyPanel 通用JavaScript函数
  3. */
  4. /* 辅助:替换路由模板中的 PLACEHOLDER */
  5. const jsRoute = (template, id) => template.replace(id ? "PLACEHOLDER" : "/PLACEHOLDER", id || "");
  6. /* -----------------------
  7. 小工具 / 辅助函数
  8. ----------------------- */
  9. /** 统一弹窗封装(基于 SweetAlert2) */
  10. function showAlert(options) {
  11. // options 直接传给 swal.fire;返回 Promise
  12. return swal.fire(options);
  13. }
  14. /** 将 errors 对象转换为 <ul> HTML 字符串 */
  15. function buildErrorHtml(errors) {
  16. let errorStr = "";
  17. Object.values(errors).forEach(values => {
  18. values.forEach(v => {
  19. errorStr += `<li>${v}</li>`;
  20. });
  21. });
  22. return `<ul>${errorStr}</ul>`;
  23. }
  24. /* -----------------------
  25. AJAX 核心
  26. ----------------------- */
  27. /**
  28. * 基础AJAX请求 - 返回 jQuery jqXHR
  29. * @param {Object} options - 请求选项
  30. * @param {string} options.url - 请求URL
  31. * @param {string} options.method - HTTP方法 (GET, POST, PUT, DELETE, PATCH)
  32. * @param {Object} options.data - 请求数据
  33. * @param {string} options.dataType - 预期服务器响应数据类型
  34. * @param {function} options.beforeSend - 请求发送前回调
  35. * @param {function} options.success - 请求成功回调
  36. * @param {function} options.error - 请求失败回调
  37. * @param {function} options.complete - 请求完成后回调(无论成功失败)
  38. */
  39. function ajaxRequest(options) {
  40. // 简化对象合并
  41. const settings = Object.assign({
  42. method: "GET",
  43. dataType: "json",
  44. data: {}
  45. }, options);
  46. // CSRF 自动注入(只在写方法上)
  47. if (["POST", "PUT", "DELETE", "PATCH"].includes(settings.method.toUpperCase()) &&
  48. typeof CSRF_TOKEN !== "undefined" &&
  49. !(settings.data && settings.data._token)) {
  50. settings.data = Object.assign({}, settings.data || {}, { _token: CSRF_TOKEN });
  51. }
  52. // loading 包装(如果提供 loadingSelector)
  53. if (settings.loadingSelector) {
  54. const origBefore = settings.beforeSend;
  55. const origComplete = settings.complete;
  56. settings.beforeSend = function (xhr, opts) {
  57. try { $(settings.loadingSelector).show(); } catch (e) { /* ignore */ }
  58. if (origBefore) origBefore.call(this, xhr, opts);
  59. };
  60. settings.complete = function (xhr, status) {
  61. try { $(settings.loadingSelector).hide(); } catch (e) { /* ignore */ }
  62. if (origComplete) origComplete.call(this, xhr, status);
  63. };
  64. }
  65. return $.ajax(settings);
  66. }
  67. /**
  68. * ajaxMethod - 为带有默认 success(handleResponse) 的方法提供便利
  69. */
  70. function ajaxMethod(method, url, data = {}, options = {}) {
  71. const opts = {...options};
  72. opts.success = opts.success ?? (ret => handleResponse(ret));
  73. return ajaxRequest({url, method, data, ...opts});
  74. }
  75. const createAjaxMethod = (method) => (url, data = {}, options = {}) => ajaxMethod(method, url, data, options);
  76. const ajaxGet = (url, data = {}, options = {}) => ajaxRequest({url, data, ...options});
  77. const ajaxPost = createAjaxMethod("POST");
  78. const ajaxPut = createAjaxMethod("PUT");
  79. const ajaxDelete = createAjaxMethod("DELETE");
  80. const ajaxPatch = createAjaxMethod("PATCH");
  81. /* -----------------------
  82. 通用弹窗 / 提示
  83. ----------------------- */
  84. /**
  85. * 显示确认对话框(基于 swal.fire)
  86. * @param {string} options.title - 对话框标题
  87. * @param {string} options.text - 对话框文本内容
  88. * @param {string} options.html - 对话框HTML内容 (优先级高于text)
  89. * @param {string} options.icon - 图标类型 (success, error, warning, info, question)
  90. * @param {string} options.cancelButtonText - 取消按钮文本
  91. * @param {string} options.confirmButtonText - 确认按钮文本
  92. * @param {function} options.onConfirm - 确认回调函数
  93. * @param {function} options.onCancel - 取消回调函数
  94. */
  95. function showConfirm(options) {
  96. const {onConfirm, onCancel, ...alertOptions} = {
  97. icon: "question",
  98. allowEnterKey: false,
  99. showCancelButton: true,
  100. cancelButtonText: typeof TRANS !== "undefined" ? TRANS.btn.close : "Cancel",
  101. confirmButtonText: typeof TRANS !== "undefined" ? TRANS.btn.confirm : "Confirm",
  102. ...options
  103. };
  104. alertOptions.title = alertOptions.title || (typeof TRANS !== "undefined" ? TRANS.confirm_title : "Confirm");
  105. if (!alertOptions.html && !alertOptions.text) {
  106. alertOptions.text = typeof TRANS !== "undefined" ? TRANS.confirm_action : "Are you sure you want to perform this action?";
  107. }
  108. showAlert(alertOptions).then((result) => {
  109. if (result.value && typeof onConfirm === "function") {
  110. onConfirm(result);
  111. } else if (!result.value && typeof onCancel === "function") {
  112. onCancel(result);
  113. }
  114. });
  115. }
  116. /**
  117. * 显示操作结果提示
  118. * @param {string} options.title - 提示标题
  119. * @param {string} options.message - 提示消息
  120. * @param {string} options.icon - 图标类型 (success, error, warning, info)
  121. * @param {boolean} options.autoClose - 是否自动关闭
  122. * @param {number} options.timer - 自动关闭时间 (毫秒)
  123. * @param {boolean} options.showConfirmButton - 是否显示确认按钮
  124. * @param {string} options.html - HTML内容
  125. * @param {function} options.callback - 关闭后回调
  126. */
  127. function showMessage(options = {}) {
  128. // 确认按钮显示逻辑:手动设置 > 自动关闭时隐藏 > 默认显示
  129. const showConfirmButton = options.showConfirmButton !== undefined
  130. ? options.showConfirmButton
  131. : false;
  132. const explicitAutoClose = options.autoClose;
  133. const hasTimer = options.timer !== undefined;
  134. const disableAutoClose = showConfirmButton === true;
  135. const isAutoClose = explicitAutoClose !== undefined
  136. ? explicitAutoClose
  137. : (hasTimer ? true : (!disableAutoClose));
  138. const timerValue = hasTimer
  139. ? options.timer
  140. : (isAutoClose ? 1500 : null);
  141. const alertOptions = {
  142. title: options.title || options.message,
  143. icon: options.icon || "info",
  144. html: options.html,
  145. showConfirmButton: showConfirmButton,
  146. ...(timerValue && isAutoClose && {timer: timerValue}),
  147. ...(options.title && options.message && !options.html && {text: options.message})
  148. };
  149. showAlert(alertOptions).then(() => {
  150. if (typeof options.callback === "function") options.callback();
  151. });
  152. }
  153. /* -----------------------
  154. 通用错误处理
  155. ----------------------- */
  156. /**
  157. * handleErrors - 处理 xhr 错误(422 验证错误 / 其它错误)
  158. * options: { validation: 'field'|'element'|'swal', default: 'swal'|'field'|'element', form, element, onError }
  159. * @param {Object} xhr - AJAX响应对象
  160. * @param {Object} options - 错误处理选项
  161. * @param {string} options.validation - 验证错误显示类型: 'field', 'element', 'swal'
  162. * @param {string} options.default - 默认错误显示类型: 'swal'(默认), 'field', 'element'
  163. * @param {string|Object} options.form - 表单选择器或jQuery对象 (type='field'时使用)
  164. * @param {string} options.element - 错误信息显示元素的选择器 (type='element'时使用)
  165. * @param {function} options.onError - 自定义错误处理回调
  166. */
  167. function handleErrors(xhr, options = {}) {
  168. const settings = Object.assign({validation: 'field', default: 'swal'}, options);
  169. if (typeof settings.onError === "function") {
  170. return settings.onError(xhr);
  171. }
  172. // 验证错误 422
  173. if (xhr.status === 422 && xhr.responseJSON?.errors) {
  174. const errors = xhr.responseJSON.errors;
  175. switch (settings.validation) {
  176. case 'field':
  177. if (settings.form) {
  178. const $form = typeof settings.form === "string" ? $(settings.form) : settings.form;
  179. $form.find(".is-invalid").removeClass("is-invalid");
  180. $form.find(".invalid-feedback").remove();
  181. Object.keys(errors).forEach(field => {
  182. const $field = $form.find(`[name="${field}"]`);
  183. if ($field.length) {
  184. $field.addClass("is-invalid");
  185. const errorMessage = errors[field][0];
  186. const $feedback = $("<div>").addClass("invalid-feedback").text(errorMessage);
  187. $field.after($feedback);
  188. }
  189. });
  190. const $firstError = $form.find(".is-invalid").first();
  191. if ($firstError.length) {
  192. $("html, body").animate({scrollTop: $firstError.offset().top - 100}, 500);
  193. }
  194. } else {
  195. // 如果没有提供 form,回退到 swal 显示
  196. showMessage({title: xhr.responseJSON.message || (typeof TRANS !== "undefined" ? TRANS.operation_failed : "Operation failed"), html: buildErrorHtml(errors), icon: "error"});
  197. }
  198. break;
  199. case 'element':
  200. if (settings.element) {
  201. $(settings.element).html(buildErrorHtml(errors)).show();
  202. } else {
  203. showMessage({title: xhr.responseJSON.message || (typeof TRANS !== "undefined" ? TRANS.operation_failed : "Operation failed"), html: buildErrorHtml(errors), icon: "error"});
  204. }
  205. break;
  206. case 'swal':
  207. default:
  208. showMessage({
  209. title: xhr.responseJSON.message || (typeof TRANS !== "undefined" ? TRANS.operation_failed : "Operation failed"),
  210. html: buildErrorHtml(errors),
  211. icon: "error"
  212. });
  213. break;
  214. }
  215. return true;
  216. }
  217. // 其它错误
  218. const errorMessage = xhr.responseJSON?.message || xhr.statusText || (typeof TRANS !== "undefined" ? TRANS.request_failed : "Request failed");
  219. // 提取公共的 showMessage 调用
  220. const showMessageOptions = {title: errorMessage, icon: "error"};
  221. switch (settings.default) {
  222. case 'element':
  223. if (settings.element) {
  224. $(settings.element).html(errorMessage).show();
  225. } else {
  226. showMessage(showMessageOptions);
  227. }
  228. break;
  229. case 'field':
  230. if (settings.form) {
  231. showMessage(showMessageOptions);
  232. } else {
  233. showMessage(showMessageOptions);
  234. }
  235. break;
  236. case 'swal':
  237. default:
  238. showMessage(showMessageOptions);
  239. break;
  240. }
  241. return false;
  242. }
  243. /* -----------------------
  244. AJAX 响应处理
  245. ----------------------- */
  246. /**
  247. * 处理AJAX响应结果
  248. * @param {Object} response - AJAX响应
  249. * @param {Object} options - 处理选项
  250. * @param {boolean} options.reload - 成功后是否刷新页面
  251. * @param {string} options.redirectUrl - 成功后重定向URL
  252. * @param {function} options.onSuccess - 成功回调
  253. * @param {function} options.onError - 错误回调
  254. * @param {boolean} options.showMessage - 是否显示消息提示
  255. * @returns {Object} 原始响应
  256. */
  257. function handleResponse(response, options = {}) {
  258. const settings = Object.assign({reload: true, showMessage: true}, options);
  259. if (response?.status === "success") {
  260. const successCallback = () => {
  261. if (settings.onSuccess) {
  262. settings.onSuccess(response);
  263. } else if (settings.redirectUrl) {
  264. window.location.href = settings.redirectUrl;
  265. } else if (settings.reload) {
  266. window.location.reload();
  267. }
  268. };
  269. if (settings.showMessage) {
  270. showMessage({
  271. title: response.message || (typeof TRANS !== "undefined" ? TRANS.operation_success : "Operation successful"),
  272. icon: "success",
  273. showConfirmButton: false,
  274. callback: successCallback
  275. });
  276. } else {
  277. successCallback();
  278. }
  279. } else {
  280. const errorCallback = () => {
  281. if (settings.onError) settings.onError(response);
  282. };
  283. if (settings.showMessage) {
  284. showMessage({
  285. title: response.message || (typeof TRANS !== "undefined" ? TRANS.operation_failed : "Operation failed"),
  286. icon: "error",
  287. showConfirmButton: true,
  288. callback: errorCallback
  289. });
  290. } else if (settings.onError) {
  291. settings.onError(response);
  292. }
  293. }
  294. return response;
  295. }
  296. /* -----------------------
  297. 其他工具函数
  298. ----------------------- */
  299. /** 重置搜索表单(清除查询参数) */
  300. function resetSearchForm() {
  301. window.location.href = window.location.href.split("?")[0];
  302. }
  303. /**
  304. * 初始化表单内 select change 时自动提交
  305. * 默认:formSelector = "form:not(.modal-body form)"
  306. */
  307. function initAutoSubmitSelects(formSelector = "form:not(.modal-body form)", excludeSelector = ".modal-body select") {
  308. // 在提交前禁用空值 input/select,防止空字符串参数传递
  309. $(formSelector).on("submit", function () {
  310. const $form = $(this);
  311. $form.find("input:not([type=\"submit\"]), select").filter(function () {
  312. return this.value === "";
  313. }).prop("disabled", true);
  314. // 提交后恢复 disabled
  315. setTimeout(() => {
  316. $form.find(":disabled").prop("disabled", false);
  317. }, 0);
  318. });
  319. // 仅绑定在指定表单内的 select
  320. $(formSelector).find("select").not(excludeSelector).on("change", function () {
  321. $(this).closest("form").trigger("submit");
  322. });
  323. }
  324. /**
  325. * 复制文本到剪贴板(优先使用 navigator.clipboard)
  326. * @param {string} text - 要复制的文本
  327. * @param {Object} options - 选项
  328. * @param {boolean} options.showMessage - 是否显示消息提示
  329. * @param {string} options.successMessage - 复制成功消息
  330. * @param {string} options.errorMessage - 复制失败消息
  331. * @param {function} options.onSuccess - 复制成功回调
  332. * @param {function} options.onError - 复制失败回调
  333. * @returns {boolean} 是否复制成功
  334. */
  335. function copyToClipboard(text, options = {}) {
  336. const settings = Object.assign({
  337. showMessage: true,
  338. successMessage: typeof TRANS !== "undefined" ? TRANS.copy.success : "Copy successful",
  339. errorMessage: typeof TRANS !== "undefined" ? TRANS.copy.failed : "Copy failed, please copy manually"
  340. }, options);
  341. if (navigator.clipboard && window.isSecureContext) {
  342. navigator.clipboard.writeText(text).then(() => {
  343. if (settings.showMessage) showMessage({title: settings.successMessage, icon: "success", autoClose: true});
  344. settings.onSuccess?.();
  345. }).catch(err => {
  346. console.error("Copy failed: ", err);
  347. if (settings.showMessage) showMessage({title: settings.errorMessage, icon: "error"});
  348. settings.onError?.(err);
  349. });
  350. return true;
  351. } else {
  352. const textarea = document.createElement("textarea");
  353. textarea.value = text;
  354. textarea.style.position = "fixed";
  355. textarea.style.opacity = 0;
  356. document.body.appendChild(textarea);
  357. textarea.select();
  358. let success = false;
  359. try {
  360. success = document.execCommand("copy");
  361. if (success && settings.showMessage) showMessage({title: settings.successMessage, icon: "success", autoClose: true});
  362. success && settings.onSuccess?.();
  363. } catch (err) {
  364. console.error("Unable to copy text: ", err);
  365. if (settings.showMessage) showMessage({title: settings.errorMessage, icon: "error"});
  366. settings.onError?.(err);
  367. }
  368. document.body.removeChild(textarea);
  369. return success;
  370. }
  371. }
  372. /* -----------------------
  373. 通用删除确认
  374. ----------------------- */
  375. /**
  376. * 通用删除确认功能
  377. * @param {string} url - 删除请求的URL
  378. * @param {string} attribute - 要删除的实体属性名称
  379. * @param {string} name - 要删除项目的ID或名称
  380. * @param {Object} options - 附加选项
  381. * @param {string} options.title - 自定义标题
  382. * @param {string} options.text - 自定义文本内容
  383. * @param {string} options.html - 自定义HTML内容
  384. * @param {string} options.icon - 自定义图标 (success, error, warning, info, question)
  385. * @param {function} options.callback - 成功后的回调函数 (等同于onSuccess)
  386. * @param {function} options.onSuccess - 成功后的回调函数
  387. * @param {function} options.onError - 错误后的回调函数
  388. * @param {boolean} options.reload - 成功后是否刷新页面
  389. * @param {string} options.redirectUrl - 成功后重定向URL
  390. */
  391. function confirmDelete(url, name, attribute, options = {}) {
  392. const defaults = {
  393. titleMessage: typeof TRANS !== "undefined" ? TRANS.warning : "Warning",
  394. };
  395. let text = options.text;
  396. if (!text && typeof TRANS !== "undefined" && TRANS.confirm?.delete) {
  397. text = TRANS.confirm.delete.replace("{attribute}", attribute || "").replace("{name}", name || "");
  398. } else if (!text) {
  399. text = typeof TRANS !== "undefined" ? (TRANS.confirm_delete || "Are you sure you want to delete {attribute} [{name}]?").replace("{attribute}", attribute || "").replace("{name}", name || "") : `Are you sure you want to delete ${attribute || ""} [${name || ""}]?`;
  400. }
  401. showConfirm({
  402. title: options.title || defaults.titleMessage,
  403. icon: options.icon || "warning",
  404. text: text,
  405. html: options.html,
  406. onConfirm: () => {
  407. ajaxDelete(url, {}, {
  408. success: (response) => {
  409. handleResponse(response, {
  410. reload: options.reload !== false,
  411. redirectUrl: options.redirectUrl,
  412. onSuccess: options.callback || options.onSuccess,
  413. onError: options.onError
  414. });
  415. }
  416. });
  417. }
  418. });
  419. }