index.blade.php 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655
  1. @extends('admin.table_layouts')
  2. @push('css')
  3. <style>
  4. .modal-body {
  5. max-height: 60vh;
  6. overflow-y: auto;
  7. }
  8. .list-icons>li {
  9. border-bottom: 1px solid #e4eaec !important;
  10. padding: 5px 8px;
  11. }
  12. .list-icons>li:last-of-type {
  13. border-bottom: none !important;
  14. }
  15. .sub-container {
  16. border-left: 2px solid #e9ecef;
  17. }
  18. .sub-container>li {
  19. padding: 8px 10px;
  20. border-bottom: 1px dashed #e9ecef !important;
  21. font-size: 0.9em;
  22. }
  23. .operation-message {
  24. max-width: 60%;
  25. word-wrap: break-word;
  26. word-break: break-all;
  27. white-space: normal;
  28. }
  29. </style>
  30. @endpush
  31. @section('content')
  32. <div class="page-content container-fluid">
  33. <x-admin.table-panel :title="trans('admin.menu.node.list')" :theads="[
  34. 'ID',
  35. trans('model.common.type'),
  36. trans('model.node.name'),
  37. trans('model.node.domain'),
  38. 'IP',
  39. trans('model.node.static'),
  40. trans('model.node.online_user'),
  41. trans('model.node.data_consume'),
  42. trans('model.node.data_rate'),
  43. trans('model.common.extend'),
  44. trans('common.status.attribute'),
  45. trans('common.action'),
  46. ]" :count="trans('admin.node.counts', ['num' => $nodeList->total()])" :pagination="$nodeList->links()" :delete-config="['url' => route('admin.node.destroy', 'PLACEHOLDER'), 'attribute' => trans('model.node.attribute'), 'nameColumn' => 2]">
  47. @canany(['admin.node.reload', 'admin.node.geo', 'admin.node.create'])
  48. <x-slot:actions>
  49. <div class="btn-group">
  50. @can('admin.node.reload')
  51. @if ($nodeList->where('type', 4)->count())
  52. <button class="btn btn-info" type="button" onclick="reload()">
  53. <i class="icon wb-reload" id="reload_all" aria-hidden="true"></i> {{ trans('admin.node.reload_all') }}
  54. </button>
  55. @endif
  56. @endcan
  57. @can('admin.node.geo')
  58. <button class="btn btn-outline-default" type="button" onclick="handleNodeAction('geo')">
  59. <i class="icon wb-map" id="geo_all" aria-hidden="true"></i> {{ trans('admin.node.refresh_geo_all') }}
  60. </button>
  61. @endcan
  62. @can('admin.node.check')
  63. <button class="btn btn-outline-primary" type="button" onclick="handleNodeAction('check')">
  64. <i class="icon wb-signal" id="check_all" aria-hidden="true"></i> {{ trans('admin.node.connection_test_all') }}
  65. </button>
  66. @endcan
  67. @can('admin.node.create')
  68. <a class="btn btn-primary" href="{{ route('admin.node.create') }}">
  69. <i class="icon wb-plus"></i> {{ trans('common.add') }}
  70. </a>
  71. @endcan
  72. </div>
  73. </x-slot:actions>
  74. @endcan
  75. <x-slot:tbody>
  76. @foreach ($nodeList as $node)
  77. <tr>
  78. <td> {{ $node->id }} </td>
  79. <td> {{ $node->type_label }} </td>
  80. <td> {{ $node->name }} </td>
  81. <td> {{ $node->server }} </td>
  82. <td> {{ $node->is_ddns ? trans('model.node.ddns') : $node->ip }} </td>
  83. <td> {{ $node->uptime }} </td>
  84. <td> {{ $node->online_users ?: '-' }} </td>
  85. <td> {{ $node->transfer }} </td>
  86. <td> {{ $node->traffic_rate }} </td>
  87. <td>
  88. @isset($node->profile['passwd'])
  89. {{-- 单端口 --}}
  90. <span class="badge badge-lg badge-info"><i class="fa-solid fa-1" aria-hidden="true"></i></span>
  91. @endisset
  92. @if ($node->is_display === 0)
  93. {{-- 节点完全不可见 --}}
  94. <span class="badge badge-lg badge-danger"><i class="icon wb-eye-close" aria-hidden="true"></i></span>
  95. @elseif($node->is_display === 1)
  96. {{-- 节点只在页面中显示 --}}
  97. <span class="badge badge-lg badge-danger"><i class="fa-solid fa-link-slash" aria-hidden="true"></i></span>
  98. @elseif($node->is_display === 2)
  99. {{-- 节点只可被订阅到 --}}
  100. <span class="badge badge-lg badge-danger"><i class="fa-solid fa-store-slash" aria-hidden="true"></i></span>
  101. @endif
  102. @if ($node->ip)
  103. <span class="badge badge-md badge-info"><i class="fa-solid fa-4" aria-hidden="true"></i></span>
  104. @endif
  105. @if ($node->ipv6)
  106. <span class="badge badge-md badge-info"><i class="fa-solid fa-6" aria-hidden="true"></i></span>
  107. @endif
  108. </td>
  109. <td>
  110. @if ($node->isOnline)
  111. @if ($node->status)
  112. {{ $node->load }}
  113. @else
  114. <i class="yellow-700 icon icon-spin fa-solid fa-gear" aria-hidden="true"></i>
  115. @endif
  116. @else
  117. @if ($node->status)
  118. <i class="red-600 fa-solid fa-gear" aria-hidden="true"></i>
  119. @else
  120. <i class="red-600 fa-solid fa-handshake-simple-slash" aria-hidden="true"></i>
  121. @endif
  122. @endif
  123. </td>
  124. <td>
  125. @canany(['admin.node.edit', 'admin.node.clone', 'admin.node.destroy', 'admin.node.monitor', 'admin.node.geo', 'admin.node.check',
  126. 'admin.node.reload'])
  127. <x-ui.dropdown>
  128. @can('admin.node.edit')
  129. <x-ui.dropdown-item :url="route('admin.node.edit', [$node->id, 'page' => Request::query('page', 1)])" icon="wb-edit" :text="trans('common.edit')" />
  130. @endcan
  131. @can('admin.node.clone')
  132. <x-ui.dropdown-item :url="route('admin.node.clone', $node)" icon="wb-copy" :text="trans('admin.clone')" />
  133. @endcan
  134. @can('admin.node.destroy')
  135. <x-ui.dropdown-item color="red-700" url="javascript:destroy('{{ $node->id }}')" icon="wb-trash" :text="trans('common.delete')" />
  136. @endcan
  137. @can('admin.node.monitor')
  138. <x-ui.dropdown-item :url="route('admin.node.monitor', $node)" icon="wb-stats-bars" :text="trans('admin.node.traffic_monitor')" />
  139. @endcan
  140. <hr />
  141. @can('admin.node.geo')
  142. <x-ui.dropdown-item id="geo_{{ $node->id }}" url="javascript:handleNodeAction('geo', '{{ $node->id }}')" icon="wb-map"
  143. :text="trans('admin.node.refresh_geo')" />
  144. @endcan
  145. @can('admin.node.check')
  146. <x-ui.dropdown-item id="node_{{ $node->id }}" url="javascript:handleNodeAction('check', '{{ $node->id }}')"
  147. icon="wb-signal" :text="trans('admin.node.connection_test')" />
  148. @endcan
  149. @if ($node->type === 4)
  150. @can('admin.node.reload')
  151. <hr />
  152. <x-ui.dropdown-item id="reload_{{ $node->id }}" url="javascript:reload({{ $node->id }})" icon="wb-reload"
  153. :text="trans('admin.node.reload')" />
  154. @endcan
  155. @endif
  156. </x-ui.dropdown>
  157. @endcan
  158. </td>
  159. </tr>
  160. @foreach ($node->childNodes as $childNode)
  161. <tr class="bg-blue-grey-200 grey-700 table-borderless">
  162. <td></td>
  163. <td><i class="float-left fa-solid fa-right-left" aria-hidden="true"></i>
  164. <strong>{{ trans('model.node.transfer') }}</strong>
  165. </td>
  166. <td> {{ $childNode->name }} </td>
  167. <td> {{ $childNode->server }} </td>
  168. <td> {{ $childNode->is_ddns ? trans('model.node.ddns') : $childNode->ip }} </td>
  169. <td colspan="2">
  170. @if ($childNode->is_display === 0)
  171. {{-- 节点完全不可见 --}}
  172. <span class="badge badge-lg badge-danger"><i class="icon wb-eye-close" aria-hidden="true"></i></span>
  173. @elseif($childNode->is_display === 1)
  174. {{-- 节点只在页面中显示 --}}
  175. <span class="badge badge-lg badge-danger"><i class="fa-solid fa-link-slash" aria-hidden="true"></i></span>
  176. @elseif($childNode->is_display === 2)
  177. {{-- 节点只可被订阅到 --}}
  178. <span class="badge badge-lg badge-danger"><i class="fa-solid fa-store-slash" aria-hidden="true"></i></span>
  179. @endif
  180. </td>
  181. <td colspan="2">
  182. @if (!$childNode->status || !$node->status)
  183. <i class="red-600 fa-solid fa-handshake-simple-slash" aria-hidden="true"></i>
  184. @endif
  185. </td>
  186. <td colspan="3">
  187. @canany(['admin.node.edit', 'admin.node.clone', 'admin.node.destroy', 'admin.node.monitor', 'admin.node.geo', 'admin.node.check'])
  188. <x-ui.dropdown>
  189. @can('admin.node.edit')
  190. <x-ui.dropdown-item :url="route('admin.node.edit', [$childNode->id, 'page' => Request::query('page', 1)])" icon="wb-edit" :text="trans('common.edit')" />
  191. @endcan
  192. @can('admin.node.clone')
  193. <x-ui.dropdown-item :url="route('admin.node.clone', $childNode)" icon="wb-copy" :text="trans('admin.clone')" />
  194. @endcan
  195. @can('admin.node.destroy')
  196. <x-ui.dropdown-item color="red-700" url="javascript:destroy('{{ $childNode->id }}')" icon="wb-trash" :text="trans('common.delete')" />
  197. @endcan
  198. @can('admin.node.monitor')
  199. <x-ui.dropdown-item :url="route('admin.node.monitor', $childNode)" icon="wb-stats-bars" :text="trans('admin.node.traffic_monitor')" />
  200. @endcan
  201. <hr />
  202. @can('admin.node.geo')
  203. <x-ui.dropdown-item id="geo_{{ $childNode->id }}" url="javascript:handleNodeAction('geo', '{{ $childNode->id }}')"
  204. icon="wb-map" :text="trans('admin.node.refresh_geo')" />
  205. @endcan
  206. @can('admin.node.check')
  207. <x-ui.dropdown-item id="node_{{ $childNode->id }}" url="javascript:handleNodeAction('check', '{{ $childNode->id }}')"
  208. icon="wb-signal" :text="trans('admin.node.connection_test')" />
  209. @endcan
  210. </x-ui.dropdown>
  211. @endcan
  212. </td>
  213. </tr>
  214. @endforeach
  215. @endforeach
  216. </x-slot:tbody>
  217. </x-admin.table-panel>
  218. </div>
  219. <!-- 节点检测结果模态框 -->
  220. <x-ui.modal id="nodeCheckModal" :title="trans('admin.node.connection_test')" size="lg">
  221. </x-ui.modal>
  222. <!-- 节点刷新地理位置结果模态框 -->
  223. <x-ui.modal id="nodeGeoRefreshModal" :title="trans('admin.node.refresh_geo')" size="lg">
  224. </x-ui.modal>
  225. <!-- 节点重载结果模态框 -->
  226. <x-ui.modal id="nodeReloadModal" :title="trans('admin.node.reload')" size="lg">
  227. </x-ui.modal>
  228. <!-- 节点删除结果模态框 -->
  229. <x-ui.modal id="nodeDeleteModal" :title="trans('admin.node.delete_operations')" size="lg">
  230. </x-ui.modal>
  231. @endsection
  232. @push('javascript')
  233. @vite(['resources/js/app.js'])
  234. <script>
  235. // 国际化配置
  236. window.i18n.extend({
  237. "broadcast": {
  238. "error": '{{ trans('common.error') }}',
  239. "websocket_unavailable": '{{ trans('common.broadcast.websocket_unavailable') }}',
  240. "websocket_disconnected": '{{ trans('common.broadcast.websocket_disconnected') }}',
  241. "setup_failed": '{{ trans('common.broadcast.setup_failed') }}',
  242. "disconnect_failed": '{{ trans('common.broadcast.disconnect_failed') }}'
  243. }
  244. });
  245. // 操作上下文管理 - 记录当前正在进行中的操作
  246. const actionContexts = {
  247. check: null,
  248. geo: null,
  249. reload: null,
  250. delete: null
  251. };
  252. // 网络状态映射
  253. const networkStatus = @json(trans('admin.network_status'));
  254. // 操作名称映射
  255. const operationNames = {
  256. "handle_ddns": '{{ trans('admin.node.operation.handle_ddns') }}',
  257. "delete_node": '{{ trans('admin.node.operation.delete_node') }}'
  258. };
  259. // 子操作名称映射
  260. const subOperationNames = {
  261. "destroy": '{{ trans('admin.node.operation.delete_domain_record') }}'
  262. };
  263. // 操作配置表
  264. const ACTION_CFG = {
  265. check: {
  266. icon: "wb-signal",
  267. routeTpl: '{{ route('admin.node.check', 'PLACEHOLDER') }}',
  268. modal: "#nodeCheckModal",
  269. btnSelector: (id) => id ? $(`#node_${id}`) : $("#check_all"),
  270. buildUI: buildCheckUI,
  271. updateUI: updateCheckUI,
  272. successMsg: '{{ trans('common.completed_item', ['attribute' => trans('admin.node.connection_test')]) }}'
  273. },
  274. geo: {
  275. icon: "wb-map",
  276. routeTpl: '{{ route('admin.node.geo', 'PLACEHOLDER') }}',
  277. modal: "#nodeGeoRefreshModal",
  278. btnSelector: (id) => id ? $(`#geo_${id}`) : $("#geo_all"),
  279. buildUI: buildNodeTableUI,
  280. updateUI: updateNodeOperationUI,
  281. successMsg: '{{ trans('common.completed_item', ['attribute' => trans('admin.node.refresh_geo')]) }}'
  282. },
  283. reload: {
  284. icon: "wb-reload",
  285. routeTpl: '{{ route('admin.node.reload', 'PLACEHOLDER') }}',
  286. modal: "#nodeReloadModal",
  287. btnSelector: (id) => id ? $(`#reload_${id}`) : $("#reload_all"),
  288. buildUI: buildNodeTableUI,
  289. updateUI: updateNodeOperationUI,
  290. successMsg: '{{ trans('common.completed_item', ['attribute' => trans('admin.node.reload')]) }}'
  291. },
  292. delete: {
  293. icon: "wb-trash",
  294. routeTpl: '{{ route('admin.node.destroy', 'PLACEHOLDER') }}',
  295. modal: "#nodeDeleteModal",
  296. btnSelector: () => {},
  297. buildUI: buildDeleteUI,
  298. updateUI: updateDeleteUI,
  299. successMsg: '{{ trans('common.completed_item', ['attribute' => trans('admin.node.delete_operations')]) }}'
  300. }
  301. };
  302. // 统一设置 spinner
  303. function setSpinner($el, iconClass, on = false) {
  304. if (!$el?.length) return;
  305. $el.removeClass(`${iconClass} wb-loop icon-spin`);
  306. $el.addClass(on ? "wb-loop icon-spin" : iconClass);
  307. }
  308. // 清理函数
  309. function cleanupActionContext(type) {
  310. const context = actionContexts[type];
  311. if (!context) return;
  312. window.broadcastingManager.unsubscribe(context.channel);
  313. actionContexts[type] = null;
  314. }
  315. // 通用操作入口
  316. function handleNodeAction(type, id) {
  317. const cfg = ACTION_CFG[type];
  318. const $btn = cfg.btnSelector(id);
  319. const channel = window.broadcastingManager.getChannelName(`node.${type}`, id);
  320. // 如果已有操作在进行中,直接显示 modal(不重新发起请求)
  321. if (actionContexts[type]) {
  322. $(cfg.modal).modal("show");
  323. return;
  324. }
  325. // 记录当前操作上下文
  326. actionContexts[type] = {
  327. actionId: id,
  328. channel: channel,
  329. $btn: $btn
  330. };
  331. setSpinner($btn, cfg.icon, true);
  332. // 订阅广播频道
  333. const success = window.broadcastingManager.subscribe(channel, ".node.actions", (e) => handleResult(type, id, e.data || e));
  334. if (!success) {
  335. setSpinner($btn, cfg.icon);
  336. actionContexts[type] = null;
  337. return;
  338. }
  339. const routeUrl = jsRoute(cfg.routeTpl, id);
  340. // AJAX 调用
  341. const ajaxOptions = {
  342. success: () => {},
  343. error: (xhr, status, error) => {
  344. window.broadcastingManager.handleAjaxError(
  345. '{{ trans('common.error') }}',
  346. `{{ trans('common.request_failed') }} ${error}: ${xhr?.responseJSON?.exception}`
  347. );
  348. setSpinner($btn, cfg.icon);
  349. cleanupActionContext(type);
  350. }
  351. };
  352. if (type === "delete") {
  353. ajaxDelete(routeUrl, {}, ajaxOptions);
  354. } else {
  355. ajaxPost(routeUrl, {}, ajaxOptions);
  356. }
  357. }
  358. // 处理广播数据
  359. function handleResult(type, id, e) {
  360. const cfg = ACTION_CFG[type];
  361. const context = actionContexts[type];
  362. if (!cfg || !context) return;
  363. if (e.list) {
  364. cfg.buildUI(e, type);
  365. } else {
  366. cfg.updateUI(e.node_id || context.actionId, e, type);
  367. // 检查是否所有操作都完成
  368. const modal = $(cfg.modal);
  369. if (modal.find(".icon-spin").length === 0) {
  370. setSpinner(context.$btn, cfg.icon);
  371. if (cfg.successMsg) {
  372. toastr.success(cfg.successMsg);
  373. }
  374. }
  375. }
  376. }
  377. function getStatusIcon(status) {
  378. return status === 1 ? `<i class="icon wb-check text-success"></i>` : `<i class="icon wb-close text-danger"></i>`;
  379. }
  380. // 通用UI构建函数
  381. function buildNodeTableUI(e, type) {
  382. const modalSelector = ACTION_CFG[type]?.modal;
  383. $(modalSelector).modal("show");
  384. let html = `<table class="table table-hover">
  385. <thead>
  386. <tr>
  387. <th>{{ trans('validation.attributes.name') }}</th>
  388. <th>{{ trans('common.status.attribute') }}</th>
  389. <th>{{ trans('validation.attributes.message') }}</th>
  390. </tr>
  391. </thead>
  392. <tbody>`;
  393. Object.entries(e.list).forEach(([nodeId, nodeName]) => {
  394. html += `<tr data-node-id="${nodeId}">
  395. <td>${nodeName}</td>
  396. <td><i class="wb-loop icon-spin"></i></td>
  397. <td></td>
  398. </tr>`;
  399. });
  400. document.querySelector(`${modalSelector} .modal-body`).innerHTML = html + "</tbody></table>";
  401. }
  402. // 通用节点操作UI更新函数
  403. function updateNodeOperationUI(nodeId, data, type) {
  404. const modalSelector = ACTION_CFG[type]?.modal;
  405. const row = document.querySelector(`${modalSelector} tr[data-node-id="${nodeId}"]`);
  406. if (!row) return;
  407. // 默认处理方式(适用于reload等简单操作)
  408. let info = data.message || "";
  409. // 特殊处理geo操作
  410. if (type === "geo" && data.status === 1 && data.original && data.update) {
  411. info = JSON.stringify(data.original) !== JSON.stringify(data.update) ?
  412. `{{ trans('common.update') }}: [${data.original.join(", ")}] => [${data.update.join(", ")}]` : '{{ trans('Not Modified') }}';
  413. } else if (type === "reload") {
  414. if (info.message) {
  415. info = info.message;
  416. } else {
  417. info = '{{ trans('common.success_item', ['attribute' => trans('admin.node.operation.reload_node')]) }}: ' + data?.success.join(', ');
  418. if (data.error && data.error.length > 0) {
  419. info += ' | {{ trans('common.failed') }}: ' + data.error.join(', ');
  420. }
  421. }
  422. }
  423. row.querySelector("td:nth-child(2)").innerHTML = getStatusIcon(data.status);
  424. row.querySelector("td:nth-child(3)").innerHTML = info;
  425. }
  426. // check UI
  427. function buildCheckUI(e) {
  428. $("#nodeCheckModal").modal("show");
  429. let html = `<div class="row">`;
  430. const columnClass = Object.keys(e.list).length > 1 ? "col-md-6" : "col-12";
  431. Object.entries(e.list).forEach(([nodeId, node]) => {
  432. html += `
  433. <div class="${columnClass}" data-node-id="${nodeId}">
  434. <h5>${node.name}</h5>
  435. <table class="table table-hover">
  436. <thead>
  437. <tr>
  438. <th>{{ trans('user.attribute.ip') }}</th>
  439. <th>ICMP</th>
  440. <th>TCP</th>
  441. </tr>
  442. </thead>
  443. <tbody>`;
  444. node.ips.forEach(ip => {
  445. html += `
  446. <tr data-ip="${ip}">
  447. <td>${ip}</td>
  448. <td><i class="wb-loop icon-spin"></i></td>
  449. <td><i class="wb-loop icon-spin"></i></td>
  450. </tr>`;
  451. });
  452. html += `</tbody></table></div>`;
  453. });
  454. document.querySelector("#nodeCheckModal .modal-body").innerHTML = html + "</div>";
  455. }
  456. function updateCheckUI(nodeId, data) {
  457. const row = document.querySelector(`#nodeCheckModal div[data-node-id="${nodeId}"] tr[data-ip="${data.ip}"]`);
  458. if (!row) return;
  459. row.querySelector("td:nth-child(2)").innerHTML = networkStatus[data.icmp] || networkStatus[4];
  460. row.querySelector("td:nth-child(3)").innerHTML = networkStatus[data.tcp] || networkStatus[4];
  461. }
  462. // delete UI
  463. function buildDeleteUI(e) {
  464. $("#nodeDeleteModal").modal("show");
  465. let html = '<ul class="list-icons">';
  466. // e.list 是数组形式: ['delete_node', 'handle_ddns']
  467. e.list.forEach(operation => {
  468. const operationName = operationNames[operation] || operation;
  469. html += `
  470. <li class="d-flex justify-content-between align-items-center" data-operation="${operation}">
  471. <i class="wb-loop icon-spin"></i>
  472. <div class="flex-grow-1">
  473. ${operationName}
  474. </div>
  475. <div class="operation-message text-muted small"></div>
  476. </li>
  477. <ul class="sub-container list-icons"></ul>`;
  478. });
  479. document.querySelector("#nodeDeleteModal .modal-body").innerHTML = html + '</ul>';
  480. }
  481. function updateDeleteUI(nodeId, data) {
  482. if (!data.operation) return;
  483. const $operationItem = $(`#nodeDeleteModal [data-operation="${data.operation}"]`);
  484. if (!$operationItem.length) return;
  485. if (!data.sub_operation || data.sub_operation === 'list') {
  486. $operationItem.find('i:first').replaceWith(getStatusIcon(data.status));
  487. }
  488. // 处理子操作(如 DDNS 操作)
  489. if (data.sub_operation) {
  490. handleDeleteSubOperation($operationItem, data);
  491. } else if (data.message) {
  492. $operationItem.find(".operation-message").text(data.message);
  493. }
  494. // 所有操作完成后显示按钮
  495. showDeleteCompletionButton();
  496. }
  497. // 处理删除操作的子操作
  498. function handleDeleteSubOperation($operationItem, data) {
  499. // 查找或创建子操作容器
  500. let $container = $operationItem.nextAll(`.sub-container`).first();
  501. if ($container.length === 0) return;
  502. // 特殊处理 DDNS 操作中的 IP 列表预显示
  503. if (data.delete) {
  504. data.delete.forEach(ip => {
  505. createSubOperationItem($container, 'destroy', ip);
  506. });
  507. } else {
  508. const subOpKey = `${data.sub_operation}_${data.data || ''}`;
  509. // 更新或创建子操作项
  510. let $item = $container.find(`[data-sub-operation="${subOpKey}"]`);
  511. $item.find('i:first').replaceWith(getStatusIcon(data.status));
  512. if (data.message) {
  513. $item.find('.operation-message').text(data.message);
  514. }
  515. }
  516. }
  517. // 创建删除操作的子操作项
  518. function createSubOperationItem($container, operation, data) {
  519. let key = operation + '_' + data;
  520. let $item = $container.find(`[data-sub-operation="${key}"]`);
  521. const opName = subOperationNames[operation] || operation;
  522. const displayText = data ? `${opName} (${data})` : opName;
  523. if ($item.length) return;
  524. $item = $(`
  525. <li class="d-flex justify-content-between align-items-center" data-sub-operation="${key}">
  526. <i class="wb-loop icon-spin"></i>
  527. <div class="flex-grow-1">
  528. ${displayText}
  529. </div>
  530. <div class="operation-message text-muted small"></div>
  531. </li>
  532. `);
  533. $container.append($item);
  534. }
  535. // 显示删除完成确认按钮
  536. function showDeleteCompletionButton() {
  537. const $modal = $("#nodeDeleteModal");
  538. if ($modal.find(".icon-spin").length !== 0 || $modal.find(".modal-footer").length > 0) return;
  539. $modal.find(".modal-content").append(`
  540. <div class="modal-footer">
  541. <button type="button" class="btn btn-primary" data-dismiss="modal">{{ trans('common.confirm') }}</button>
  542. </div>`);
  543. }
  544. @can('admin.node.reload')
  545. function reload(id = null) {
  546. if (actionContexts['reload']) {
  547. $(ACTION_CFG['reload'].modal).modal("show");
  548. } else {
  549. showConfirm({
  550. title: '{{ trans('admin.node.reload_confirm') }}',
  551. onConfirm: () => handleNodeAction("reload", id)
  552. });
  553. }
  554. }
  555. @endcan
  556. @can('admin.node.destroy')
  557. function destroy(id = null) {
  558. const nodeName = $(`tr:has(td:first-child:contains('${id}')) td:nth-child(3)`).text().trim() || id || "";
  559. showConfirm({
  560. title: '{{ trans('common.warning') }}',
  561. text: i18n("confirm.delete")
  562. .replace("{attribute}", '{{ trans('model.node.attribute') }}')
  563. .replace("{name}", nodeName),
  564. icon: "warning",
  565. onConfirm: () => handleNodeAction("delete", id)
  566. });
  567. }
  568. @endcan
  569. // 检测、地理位置、重载 modal 的通用处理
  570. Object.keys(ACTION_CFG).forEach(type => {
  571. const modalSelector = ACTION_CFG[type].modal;
  572. $(document).on("hidden.bs.modal", modalSelector, function() {
  573. const context = actionContexts[type];
  574. const modalBody = document.querySelector(`${modalSelector} .modal-body`);
  575. const isLoading = modalBody && modalBody.querySelectorAll('.icon-spin').length > 0;
  576. if (!isLoading && context) {
  577. cleanupActionContext(type);
  578. // 清空 modal 内容
  579. if (modalBody) {
  580. modalBody.innerHTML = '';
  581. }
  582. if (type === 'delete') {
  583. location.reload();
  584. }
  585. }
  586. });
  587. });
  588. </script>
  589. @endpush