info.blade.php 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
  1. @extends('admin.layouts')
  2. @section('css')
  3. <link href="/assets/global/vendor/bootstrap-select/bootstrap-select.min.css" rel="stylesheet">
  4. <link href="/assets/global/vendor/bootstrap-datepicker/bootstrap-datepicker.min.css" rel="stylesheet">
  5. <link href="/assets/global/vendor/switchery/switchery.min.css" rel="stylesheet">
  6. <style>
  7. .hidden {
  8. display: none
  9. }
  10. .bootstrap-select .dropdown-menu {
  11. max-height: 50vh !important;
  12. }
  13. .list-icons>li {
  14. border-bottom: 1px solid #e4eaec !important;
  15. padding: 5px 8px;
  16. }
  17. .list-icons>li:last-of-type {
  18. border-bottom: none !important;
  19. }
  20. .sub-container {
  21. border-left: 2px solid #e9ecef;
  22. }
  23. .sub-container>li {
  24. padding: 8px 10px;
  25. border-bottom: 1px dashed #e9ecef !important;
  26. font-size: 0.9em;
  27. }
  28. .operation-message {
  29. max-width: 60%;
  30. word-wrap: break-word;
  31. word-break: break-all;
  32. white-space: normal;
  33. }
  34. </style>
  35. @endsection
  36. @section('content')
  37. <div class="page-content container-fluid">
  38. <x-ui.panel :title="trans(isset($node) ? 'admin.action.edit_item' : 'admin.action.add_item', ['attribute' => trans('model.node.attribute')])">
  39. <x-alert type="info" :message="trans('admin.node.info.hint')" />
  40. <x-admin.form.container handler="Submit()" enctype="true">
  41. <div class="row">
  42. <div class="col-lg-6">
  43. <h4 class="example-title">{{ trans('admin.node.info.basic') }}</h4>
  44. <x-admin.form.input name="is_ddns" type="checkbox" :label="trans('model.node.ddns')" attribute="data-plugin=switchery onchange=switchSetting('is_ddns')"
  45. :help="trans('admin.node.info.ddns_hint')" />
  46. <x-admin.form.input name="name" :label="trans('model.node.name')" required />
  47. <x-admin.form.input name="server" :label="trans('model.node.domain')" :placeholder="trans('admin.node.info.domain_placeholder')" :help="trans('admin.node.info.domain_hint')" />
  48. <x-admin.form.input name="ip" :label="trans('model.node.ipv4')" :placeholder="trans('admin.node.info.ipv4_placeholder')" required :help="trans('admin.node.info.ipv4_hint')" />
  49. <x-admin.form.input name="ipv6" :label="trans('model.node.ipv6')" :placeholder="trans('admin.node.info.ipv6_placeholder')" :help="trans('admin.node.info.ipv6_hint')" />
  50. <x-admin.form.input name="push_port" type="number" :label="trans('model.node.push_port')" :help="trans('admin.node.info.push_port_hint')" />
  51. <x-admin.form.input name="traffic_rate" type="number" :label="trans('model.node.data_rate')" step="0.01" required :help="trans('admin.node.info.data_rate_hint')" />
  52. <x-admin.form.select name="level" :label="trans('model.common.level')" :options="$levels" :help="trans('admin.node.info.level_hint')" />
  53. <x-admin.form.select name="rule_group_id" :label="trans('model.rule_group.attribute')" :options="$ruleGroups" :placeholder="trans('common.none')" />
  54. <x-admin.form.input-group name="speed_limit" type="number" :label="trans('model.node.traffic_limit')" append="Mbps" required />
  55. <x-admin.form.input name="client_limit" type="number" :label="trans('model.node.client_limit')" required />
  56. <x-admin.form.input name="sort" type="number" :label="trans('model.common.sort')" required />
  57. <x-admin.form.select name="labels" :label="trans('model.node.label')" :options="$labels" multiple />
  58. <x-admin.form.select name="country_code" :label="trans('model.node.country')">
  59. @foreach ($countries as $country)
  60. <option data-icon="fi fis fi-{{ $country->code }}" value="{{ $country->code }}">
  61. {{ strtoupper($country->code) . ' - ' . $country->name }}
  62. </option>
  63. @endforeach
  64. </x-admin.form.select>
  65. <!-- 节点 细则部分 -->
  66. <x-admin.form.input-group name="next_renewal_date" attribute="data-plugin=datepicker" :label="trans('model.node.next_renewal_date')" />
  67. <x-admin.form.skeleton name="subscription_term_value" :label="trans('model.node.subscription_term')">
  68. <div class="input-group">
  69. <input class="form-control" id="subscription_term_value" type="number" min="1" />
  70. <select class="form-control" id="subscription_term_unit" data-plugin="selectpicker" data-style="btn-outline btn-primary">
  71. <option value="days">{{ ucfirst(trans('validation.attributes.day')) }}</option>
  72. <option value="months">{{ ucfirst(trans('validation.attributes.month')) }}</option>
  73. <option value="years">{{ ucfirst(trans('validation.attributes.year')) }}</option>
  74. </select>
  75. </div>
  76. </x-admin.form.skeleton>
  77. <x-admin.form.input name="renewal_cost" type="number" :label="trans('model.node.renewal_cost')" step="0.01" />
  78. <x-admin.form.textarea name="description" :label="trans('model.common.description')" />
  79. </div>
  80. <div class="col-lg-6">
  81. <h4 class="example-title">{{ trans('admin.node.info.extend') }}</h4>
  82. <x-admin.form.radio-group name="is_display" :label="trans('model.node.display')" :options="[
  83. 0 => trans('admin.node.info.display.invisible'),
  84. 1 => trans('admin.node.info.display.node'),
  85. 2 => trans('admin.node.info.display.sub'),
  86. 3 => trans('admin.node.info.display.all'),
  87. ]" :help="trans('admin.node.info.display.hint')" />
  88. <x-admin.form.radio-group name="detection_type" :label="trans('model.node.detection')" :options="[
  89. 0 => trans('common.close'),
  90. 1 => trans('admin.node.info.detection.tcp'),
  91. 2 => trans('admin.node.info.detection.icmp'),
  92. 3 => trans('admin.node.info.detection.all'),
  93. ]" :help="trans('admin.node.info.detection.hint')" />
  94. <!-- 中转 设置部分 -->
  95. <x-admin.form.select name="relay_node_id" :label="trans('model.node.transfer')" :options="$nodes" :placeholder="trans('common.none')" />
  96. <hr />
  97. <div class="relay-config">
  98. <x-admin.form.input name="port" type="number" :label="trans('model.node.relay_port')" />
  99. </div>
  100. <!-- 代理 设置部分 -->
  101. <div class="proxy-config">
  102. <x-admin.form.radio-group name="type" :label="trans('model.common.type')" :options="config('common.proxy_protocols')" />
  103. <hr />
  104. <!-- SS/SSR 设置部分 -->
  105. <div class="ss-setting">
  106. <x-admin.form.select name="method" :label="trans('model.node.method')" :options="$methods" />
  107. <!-- TODO: Supporting SS plugin -->
  108. {{-- <x-admin.form.select name="plugin" :label="trans('model.node.plugin')" :options="['none'=>'None', 'kcptun'=>'Kcptun', 'v2ray-plugin' => 'V2ray-plugin', 'cloak'=> 'Cloak', 'shadow-tls' => 'Shadow-tls']" /> --}}
  109. {{-- <x-admin.form.textarea name="plugin_opts" :label="trans('model.node.plugin_opts')" /> --}}
  110. <x-admin.form.input name="passwd" :label="trans('model.node.service_password')" />
  111. <x-admin.form.input name="single" type="checkbox" :label="trans('model.node.single')"
  112. attribute="data-plugin=switchery onchange=switchSetting('single')" />
  113. <div class="single-setting">
  114. <x-admin.form.input name="port" type="number" :label="trans('model.node.service_port')" :help="trans('admin.node.info.single_hint')" />
  115. </div>
  116. <hr />
  117. <div class="ssr-setting">
  118. <x-admin.form.select name="protocol" :label="trans('model.node.protocol')" :options="$protocols" />
  119. <x-admin.form.textarea name="protocol_param" :label="trans('model.node.protocol_param')" />
  120. <x-admin.form.select name="obfs" :label="trans('model.node.obfs')" :options="$obfs" />
  121. <x-admin.form.textarea name="obfs_param" :label="trans('model.node.obfs_param')" :placeholder="trans('admin.node.info.obfs_param_hint')" />
  122. <x-admin.form.skeleton name="proxy_info" :label="trans('admin.node.proxy_info')">
  123. <div class="text-help">
  124. {!! trans('admin.node.proxy_info_hint') !!}
  125. </div>
  126. </x-admin.form.skeleton>
  127. <hr />
  128. </div>
  129. </div>
  130. <!-- V2ray TODO: Supporting new feature -->
  131. <div class="v2ray-setting">
  132. <x-admin.form.input name="v2_alter_id" :label="trans('model.node.v2_alter_id')" />
  133. <x-admin.form.input name="port" type="number" :label="trans('model.node.service_port')" />
  134. <x-admin.form.select name="v2_method" :label="trans('model.node.method')" :options="[
  135. 'none' => 'none',
  136. 'auto' => 'auto',
  137. 'zero' => 'zero',
  138. 'aes-128-gcm' => 'aes-128-gcm',
  139. 'chacha20-poly1305' => 'chacha20-poly1305',
  140. ]" :help="trans('admin.node.info.v2_method_hint')" />
  141. <x-admin.form.select name="v2_net" :label="trans('model.node.v2_net')" :options="[
  142. 'tcp' => 'TCP',
  143. 'kcp' => 'mKCP',
  144. 'ws' => 'WebSocket',
  145. 'httpupgrade' => 'HTTPUpgrade',
  146. 'xhttp' => 'xHTTP',
  147. 'h2' => 'HTTP/2',
  148. 'quic' => 'QUIC',
  149. 'domainsocket' => 'DomainSocket',
  150. 'grpc' => 'gRPC',
  151. ]" :help="trans('admin.node.info.v2_net_hint')" />
  152. <x-admin.form.select name="v2_type" :label="trans('model.node.v2_cover')" :options="[
  153. 'none' => trans('admin.node.info.v2_cover.none'),
  154. 'http' => trans('admin.node.info.v2_cover.http'),
  155. 'srtp' => trans('admin.node.info.v2_cover.srtp'),
  156. 'utp' => trans('admin.node.info.v2_cover.utp'),
  157. 'wechat-video' => trans('admin.node.info.v2_cover.wechat'),
  158. 'dtls' => trans('admin.node.info.v2_cover.dtls'),
  159. 'wireguard' => trans('admin.node.info.v2_cover.wireguard'),
  160. ]" />
  161. <x-admin.form.input name="v2_host" :label="trans('model.node.v2_host')" :help="trans('admin.node.info.v2_host_hint')" />
  162. <x-admin.form.input name="v2_path" :label="trans('model.node.v2_path')" />
  163. <x-admin.form.input name="v2_sni" :label="trans('model.node.v2_sni')" />
  164. <x-admin.form.input name="v2_tls" type="checkbox" :label="trans('model.node.v2_tls')"
  165. attribute="data-plugin=switchery onchange=switchSetting('v2_tls')" />
  166. <x-admin.form.input name="tls_provider" :label="trans('model.node.v2_tls_provider')" :help="trans('admin.node.info.v2_tls_provider_hint')" />
  167. {{-- <x-admin.form.input name="mux" type="checkbox" :label="trans('model.node.mux')" attribute="data-plugin=switchery onchange=switchSetting('mux')" --}}
  168. {{-- :help="trans('admin.node.info.mux')" /> --}}
  169. </div>
  170. <!-- Trojan 设置部分 -->
  171. <div class="trojan-setting">
  172. <x-admin.form.input name="port" type="number" :label="trans('model.node.service_port')" />
  173. <x-admin.form.input name="allow_insecure" type="checkbox" :label="trans('model.node.allow_insecure')" attribute="data-plugin=switchery" />
  174. </div>
  175. <!-- Hysteria2 设置部分 -->
  176. <div class="hy2-setting">
  177. <x-admin.form.input name="port" type="number" :label="trans('model.node.service_port')" />
  178. <x-admin.form.input name="ports" :label="trans('model.node.ports')" :help="trans('admin.node.info.ports')" />
  179. <x-admin.form.select name="obfs" :label="trans('model.node.obfs')" :placeholder="trans('admin.node.info.v2_cover.none')" :options="['salamander' => 'Salamander']" />
  180. <x-admin.form.input name="obfs_param" :label="trans('model.node.obfs_param')" :placeholder="trans('model.node.service_password')" />
  181. <x-admin.form.input name="upload_mbps" type="number" :label="trans('model.node.upload_mbps')" append="Mbps" />
  182. <x-admin.form.input name="download_mbps" type="number" :label="trans('model.node.download_mbps')" append="Mbps" />
  183. <x-admin.form.input name="ignore_client_bandwidth" type="checkbox" :label="trans('model.node.ignore_client_bandwidth')" attribute="data-plugin=switchery" />
  184. <x-admin.form.input name="allow_insecure" type="checkbox" :label="trans('model.node.allow_insecure')" attribute="data-plugin=switchery" />
  185. </div>
  186. </div>
  187. <x-admin.form.input name="is_udp" type="checkbox" :label="trans('model.node.udp')" attribute="data-plugin=switchery" />
  188. <x-admin.form.input name="status" type="checkbox" :label="trans('common.status.attribute')" attribute="data-plugin=switchery" />
  189. <div class="col-12 form-actions text-right">
  190. <a class="btn btn-secondary" href="{{ route('admin.node.index') }}">{{ trans('common.back') }}</a>
  191. <button class="btn btn-success" type="submit">{{ trans('common.submit') }}</button>
  192. </div>
  193. </div>
  194. </div>
  195. </x-admin.form.container>
  196. </x-ui.panel>
  197. </div>
  198. <!-- 节点结果模态框 -->
  199. <x-ui.modal id="nodeModal" :title="isset($node) ? trans('admin.node.create_operations') : trans('admin.node.update_operations')" size="lg">
  200. </x-ui.modal>
  201. @endsection
  202. @section('javascript')
  203. <script src="/assets/global/vendor/bootstrap-select/bootstrap-select.min.js"></script>
  204. <script src="/assets/global/vendor/bootstrap-datepicker/bootstrap-datepicker.min.js"></script>
  205. @if (app()->getLocale() !== 'en')
  206. <script src="/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.{{ str_replace('_', '-', app()->getLocale()) }}.min.js" charset="UTF-8">
  207. </script>
  208. @endif
  209. <script src="/assets/global/js/Plugin/bootstrap-select.js"></script>
  210. <script src="/assets/global/js/Plugin/bootstrap-datepicker.js"></script>
  211. <script src="/assets/global/vendor/switchery/switchery.min.js"></script>
  212. <script src="/assets/global/js/Plugin/switchery.js"></script>
  213. @vite(['resources/js/app.js'])
  214. <script>
  215. window.i18n.extend({
  216. 'broadcast': {
  217. 'error': '{{ trans('common.error') }}',
  218. 'websocket_unavailable': '{{ trans('common.broadcast.websocket_unavailable') }}',
  219. 'websocket_disconnected': '{{ trans('common.broadcast.websocket_disconnected') }}',
  220. 'setup_failed': '{{ trans('common.broadcast.setup_failed') }}',
  221. 'disconnect_failed': '{{ trans('common.broadcast.disconnect_failed') }}'
  222. }
  223. });
  224. const string = "{{ strtolower(Str::random()) }}";
  225. // 使用 broadcastingManager 管理广播连接
  226. const operationNames = {
  227. 'save_node_info': '{{ trans('admin.node.operation.save_node_info') }}',
  228. 'create_auth': '{{ trans('admin.node.operation.create_auth') }}',
  229. 'sync_labels': '{{ trans('admin.node.operation.sync_labels') }}',
  230. 'handle_ddns': '{{ trans('admin.node.operation.handle_ddns') }}',
  231. 'reload_node': '{{ trans('admin.node.operation.reload_node') }}',
  232. 'refresh_geo': '{{ trans('admin.node.operation.refresh_geo') }}'
  233. };
  234. // 子操作名称映射
  235. const subOperationNames = {
  236. 'store': '{{ trans('admin.node.operation.store_domain_record') }}',
  237. 'destroy': '{{ trans('admin.node.operation.delete_domain_record') }}',
  238. 'unchanged': '{{ trans('admin.node.operation.unchanged') }}',
  239. };
  240. function calculateNextNextRenewalDate() {
  241. const nextRenewalDate = $("#next_renewal_date").val();
  242. const termValue = parseInt($("#subscription_term_value").val() || 0);
  243. const termUnit = $("#subscription_term_unit").val();
  244. const nextNextRenewalDate = $("#next_next_renewal_date");
  245. if (!nextRenewalDate || termValue <= 0) {
  246. nextNextRenewalDate.val("");
  247. return;
  248. }
  249. const currentDate = new Date(nextRenewalDate);
  250. const originalDay = currentDate.getDate();
  251. if (termUnit === "months") {
  252. // 获取当前月份和年份
  253. let targetMonth = currentDate.getMonth() + termValue;
  254. let targetYear = currentDate.getFullYear() + Math.floor(targetMonth / 12);
  255. targetMonth = targetMonth % 12;
  256. // 先将日期设置为目标月的同一天
  257. currentDate.setFullYear(targetYear, targetMonth, originalDay);
  258. // 检查是否因月份天数不同而被自动调整
  259. if (currentDate.getMonth() !== targetMonth) {
  260. // 如果被调整,说明目标月份的天数比原始日期少
  261. // 将日期设置为目标月份的最后一天
  262. currentDate.setFullYear(targetYear, targetMonth + 1, 0);
  263. }
  264. } else {
  265. // 处理天数和年份的情况
  266. const adjustments = {
  267. days: "Date",
  268. years: "FullYear"
  269. };
  270. currentDate[`set${adjustments[termUnit]}`](
  271. currentDate[`get${adjustments[termUnit]}`]() + termValue
  272. );
  273. }
  274. // 显示计算结果(如果需要)
  275. if ($("#next_next_renewal_date").length) {
  276. nextNextRenewalDate.val(currentDate.toISOString().split("T")[0]);
  277. }
  278. }
  279. $(document).ready(function() {
  280. // 初始化UI元素
  281. initializeUI();
  282. // 准备节点数据
  283. let nodeData = {
  284. is_ddns: 0,
  285. push_port: 1080,
  286. traffic_rate: 1.0,
  287. level: 0,
  288. speed_limit: 1000,
  289. client_limit: 1000,
  290. is_display: 3,
  291. detection_type: 0,
  292. is_udp: 1,
  293. status: 1,
  294. sort: 1,
  295. method: '{{ $methodDefault }}',
  296. protocol: '{{ $protocolDefault }}',
  297. obfs: '{{ $obfsDefault }}',
  298. relay_node_id: '',
  299. type: 1
  300. };
  301. @isset($node)
  302. // 反向解析节点数据以适配表单字段
  303. const node = @json($node);
  304. nodeData = {
  305. single: node.type === 0 || node.type === 1 || node.type === 4 ? (node.passwd ? 1 : 0) : undefined,
  306. ...node,
  307. v2_tls: node.type === 2 ? (node?.v2_tls === 'tls' ? 1 : 0) : undefined,
  308. };
  309. // 处理订阅期限字段
  310. if (node.subscription_term) {
  311. const [value, unit] = node.subscription_term.split(" ");
  312. nodeData.subscription_term_value = value;
  313. nodeData.subscription_term_unit = unit;
  314. }
  315. setupBroadcastChannel('update', node.id);
  316. @else
  317. setupBroadcastChannel('create');
  318. @endisset
  319. // 绑定事件
  320. bindEvents();
  321. // 自动填充表单
  322. autoPopulateForm(nodeData);
  323. calculateNextNextRenewalDate();
  324. });
  325. function initializeUI() {
  326. $(".single-setting").hide();
  327. $("#v2_path").val("/" + string);
  328. }
  329. function bindEvents() {
  330. $("input:radio[name='type']").on("change", updateServiceType);
  331. $("select[name='obfs']").on("changed.bs.select", toggleObfsParam);
  332. $("#relay_node_id").on("changed.bs.select", toggleRelayConfig);
  333. $("#v2_net").on("changed.bs.select", updateV2RaySettings);
  334. $(document).on("change", "#next_renewal_date, #subscription_term_value, #subscription_term_unit",
  335. calculateNextNextRenewalDate);
  336. }
  337. // 建立广播频道连接
  338. function setupBroadcastChannel(actionType, nodeId = null) {
  339. // 使用 broadcastingManager 订阅频道
  340. window.broadcastingManager.subscribe(
  341. window.broadcastingManager.getChannelName(`node.${actionType}`, nodeId),
  342. '.node.actions',
  343. (e) => handleEditProgress(e.data || e)
  344. );
  345. }
  346. // 处理编辑进度更新
  347. function handleEditProgress(data) {
  348. if (data.list) {
  349. showOperationList(data.list);
  350. } else if (data.operation) {
  351. updateOperationStatus(data);
  352. }
  353. }
  354. // 显示操作清单
  355. function showOperationList(operationList) {
  356. $('#nodeModal').modal('show');
  357. let html = '<ul class="list-icons">';
  358. operationList.forEach(operation => {
  359. const opName = operationNames[operation] || operation;
  360. html += `
  361. <li class="d-flex justify-content-between align-items-center" data-operation="${operation}">
  362. <i class="wb-loop icon-spin"></i>
  363. <div class="flex-grow-1">
  364. ${opName}
  365. </div>
  366. <div class="operation-message text-muted small"></div>
  367. </li>
  368. <ul class="sub-container list-icons"></ul>
  369. `;
  370. });
  371. $('#nodeModal .modal-body').html(html + '</ul>');
  372. }
  373. function getStatusIcon(status) {
  374. return status === 1 ? `<i class="icon wb-check text-success"></i>` :
  375. `<i class="icon wb-close text-danger"></i>`;
  376. }
  377. // 更新操作状态
  378. function updateOperationStatus(data) {
  379. const $operationItem = $(`#nodeModal [data-operation="${data.operation}"]`);
  380. if (!$operationItem.length) return;
  381. if (!data.sub_operation || data.sub_operation === 'list' || data.sub_operation === 'unchanged') {
  382. $operationItem.find('i:first').replaceWith(getStatusIcon(data.status));
  383. }
  384. // 处理子操作(如 DDNS 操作、IP列表等)
  385. if (data.sub_operation) {
  386. handleSubOperation($operationItem, data);
  387. } else if (data.message) {
  388. $operationItem.find(".operation-message").text(data.message);
  389. }
  390. // 检查是否所有操作都已完成
  391. showCompletionButton();
  392. }
  393. function handleSubOperation($operationItem, data) {
  394. // 查找或创建子操作容器
  395. let $container = $operationItem.nextAll(`.sub-container`).first();
  396. if ($container.length === 0) return;
  397. // 特殊处理 DDNS 操作中的 IP 列表预显示
  398. if (data.add || data.delete) {
  399. data.add?.forEach(ip => {
  400. createSubOperationItem($container, 'store', ip);
  401. });
  402. data.delete?.forEach(ip => {
  403. createSubOperationItem($container, 'destroy', ip);
  404. });
  405. } else {
  406. const subOpKey = `${data.sub_operation}_${data.data || ''}`;
  407. // 更新或创建子操作项
  408. let $item = $container.find(`[data-sub-operation="${subOpKey}"]`);
  409. $item.find('i:first').replaceWith(getStatusIcon(data.status));
  410. if (data.message) {
  411. $item.find('.operation-message').text(data.message);
  412. }
  413. }
  414. }
  415. // 创建或更新子操作项的辅助函数
  416. function createSubOperationItem($container, operation, data) {
  417. let key = operation + '_' + data;
  418. let $item = $container.find(`[data-sub-operation="${key}"]`);
  419. const opName = subOperationNames[operation] || operation;
  420. const displayText = data ? `${opName} (${data})` : opName;
  421. if ($item.length) return;
  422. $item = $(`
  423. <li class="d-flex justify-content-between align-items-center" data-sub-operation="${key}">
  424. <i class="wb-loop icon-spin"></i>
  425. <div class="flex-grow-1">
  426. ${displayText}
  427. </div>
  428. <div class="operation-message text-muted small"></div>
  429. </li>
  430. `);
  431. $container.append($item);
  432. }
  433. // 显示完成确认按钮
  434. function showCompletionButton() {
  435. const $modal = $('#nodeModal');
  436. if ($modal.find(".icon-spin").length !== 0 || $modal.find(".modal-footer").length > 0) return;
  437. $modal.find(".modal-content").append(`
  438. <div class="modal-footer">
  439. <button type="button" class="btn btn-primary" data-dismiss="modal">{{ trans('common.confirm') }}</button>
  440. </div>`);
  441. }
  442. // 同时绑定模态框关闭事件
  443. $(document).on("hidden.bs.modal", '#nodeModal', function() {
  444. @isset($node)
  445. location.reload();
  446. @else
  447. window.location.href = '{{ route('admin.node.index') }}';
  448. @endisset
  449. });
  450. function switchSetting(id) {
  451. const check = document.getElementById(id).checked;
  452. if (id === "single") {
  453. $(".single-setting").toggle(check);
  454. $("#single_port").attr({
  455. "hidden": !check,
  456. "required": check
  457. });
  458. if (!check) $("#passwd").val("");
  459. } else if (id === "is_ddns") {
  460. $("#ip, #ipv6").attr("readonly", check).val("");
  461. $("#server").attr("required", check);
  462. }
  463. }
  464. // 设置服务类型
  465. function updateServiceType() {
  466. const type = parseInt($(this).val());
  467. const settingsMap = {
  468. 0: [".ss-setting"],
  469. 1: [".ss-setting", ".ssr-setting"],
  470. 2: [".v2ray-setting", "#v2_port"],
  471. 3: [".trojan-setting", "#trojan_port"],
  472. 4: [".ss-setting", ".ssr-setting"],
  473. 5: [".hy2-setting", "#hy2_port"]
  474. };
  475. $(".ss-setting, .ssr-setting, .v2ray-setting, .trojan-setting, .hy2-setting").hide();
  476. Object.keys(settingsMap).forEach(key => $(settingsMap[key].join(",")).hide());
  477. (settingsMap[type] || []).forEach(selector => $(selector).show());
  478. }
  479. function toggleObfsParam() {
  480. const $obfsSelect = $(this);
  481. const $parentContainer = $obfsSelect.closest('.ssr-setting, .hy2-setting');
  482. const $obfsParam = $parentContainer.find('[name="obfs_param"]').first();
  483. const show = $obfsSelect.val() && $obfsSelect.val() !== "plain";
  484. $obfsParam.closest('.form-group').toggle(show);
  485. if (!show) $obfsParam.val("");
  486. }
  487. function toggleRelayConfig() {
  488. const hasRelay = $("#relay_node_id").val() !== "";
  489. $(".relay-config").toggle(hasRelay);
  490. $(".proxy-config").toggle(!hasRelay);
  491. $("#relay_port").attr({
  492. hidden: !hasRelay,
  493. required: hasRelay
  494. });
  495. }
  496. // 设置V2Ray详细设置
  497. function updateV2RaySettings() {
  498. const net = $(this).val();
  499. const $type = $(".v2_type");
  500. const $typeOption = $("#type_option");
  501. const $host = $(".v2_host");
  502. const $path = $("#v2_path");
  503. $type.show();
  504. $host.show();
  505. if (!$path.val()) {
  506. $path.val("/" + string);
  507. }
  508. switch (net) {
  509. case "ws":
  510. case "http":
  511. $type.hide();
  512. break;
  513. case "domainsocket":
  514. $type.hide();
  515. $host.hide();
  516. break;
  517. case "quic":
  518. $typeOption.attr("disabled", false);
  519. if (!$path.val()) {
  520. $path.val(string);
  521. }
  522. break;
  523. case "kcp":
  524. case "tcp":
  525. default:
  526. $typeOption.attr("disabled", true);
  527. break;
  528. }
  529. $("#v2_type").selectpicker("refresh");
  530. }
  531. // ajax同步提交
  532. function Submit() {
  533. // 防止重复提交
  534. const $submitBtn = $('.form-horizontal button[type="submit"]');
  535. if ($submitBtn.hasClass('disabled')) {
  536. return false;
  537. }
  538. // 禁用提交按钮以防止重复提交
  539. $submitBtn.addClass('disabled').prop('disabled', true);
  540. // 收集表单数据
  541. const data = collectFormData('.form-horizontal');
  542. // 拼接 subscription_term
  543. const termValue = $("#subscription_term_value").val();
  544. const termUnit = $("#subscription_term_unit").val();
  545. data["subscription_term"] = termValue ? `${termValue} ${termUnit}` : null;
  546. // 发送 AJAX 请求
  547. ajaxRequest({
  548. url: '{{ isset($node) ? route('admin.node.update', $node['id']) : route('admin.node.store') }}',
  549. method: '{{ isset($node) ? 'PUT' : 'POST' }}',
  550. data: data,
  551. success: function(ret) {
  552. // 成功消息处理在广播中完成
  553. if (ret.status !== 'success') {
  554. // 隐藏可能已经显示的 modal
  555. $('#nodeModal').modal('hide');
  556. handleResponse(ret, {
  557. redirectUrl: '{{ route('admin.node.index') . (Request::getQueryString() ? '?' . Request::getQueryString() : '') }}',
  558. });
  559. }
  560. },
  561. error: function(xhr) {
  562. // 隐藏可能已经显示的 modal
  563. $('#nodeModal').modal('hide');
  564. // 处理验证错误
  565. handleErrors(xhr, {
  566. form: '.form-horizontal'
  567. });
  568. },
  569. complete: function() {
  570. $submitBtn.removeClass('disabled').prop('disabled', false);
  571. }
  572. });
  573. return false;
  574. }
  575. // 服务条款
  576. window.showTnc = function() {
  577. const jsonConfig = {
  578. "additional_ports": {
  579. "443": {
  580. "passwd": "ProxyPanel",
  581. "method": "none",
  582. "protocol": "auth_chain_a",
  583. "protocol_param": "#",
  584. "obfs": "plain",
  585. "obfs_param": "fe2.update.microsoft.com"
  586. }
  587. }
  588. };
  589. swal.fire({
  590. title: "[节点 user-config.json 配置示例]",
  591. width: "36em",
  592. html: `
  593. <div class="text-left">
  594. <ol>
  595. <li>请勿直接复制黏贴以下配置,SSR(R)会报错的</li>
  596. <li>确保服务器时间为CST</li>
  597. </ol>
  598. <pre class="bg-grey-800 text-white">${JSON.stringify(jsonConfig, null, 2)}</pre>
  599. </div>
  600. `,
  601. icon: "info"
  602. });
  603. };
  604. // 模式提示
  605. window.showPortsOnlyConfig = function() {
  606. swal.fire({
  607. title: "[节点 user-config.json 配置示例]",
  608. width: "36em",
  609. html: `
  610. <ul class="bg-grey-800 text-white text-left">
  611. <li>严格模式:"additional_ports_only": "true"</li>
  612. <li>兼容模式:"additional_ports_only": "false"</li>
  613. </ul>
  614. `,
  615. icon: "info"
  616. });
  617. };
  618. </script>
  619. @endsection