1
0

info.blade.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  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. </style>
  14. @endsection
  15. @section('content')
  16. <div class="page-content container-fluid">
  17. <x-ui.panel :title="trans(isset($node) ? 'admin.action.edit_item' : 'admin.action.add_item', ['attribute' => trans('model.node.attribute')])">
  18. <x-alert type="info" :message="trans('admin.node.info.hint')" />
  19. <x-admin.form.container>
  20. <div class="row">
  21. <div class="col-lg-6">
  22. <h4 class="example-title">{{ trans('admin.node.info.basic') }}</h4>
  23. <x-admin.form.input name="is_ddns" type="checkbox" :label="trans('model.node.ddns')" attribute="data-plugin=switchery onchange=switchSetting('is_ddns')"
  24. :help="trans('admin.node.info.ddns_hint')" />
  25. <x-admin.form.input name="name" :label="trans('model.node.name')" required />
  26. <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')" />
  27. <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')" />
  28. <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')" />
  29. <x-admin.form.input name="push_port" type="number" :label="trans('model.node.push_port')" :help="trans('admin.node.info.push_port_hint')" />
  30. <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')" />
  31. <x-admin.form.select name="level" :label="trans('model.common.level')" :options="$levels" :help="trans('admin.node.info.level_hint')" />
  32. <x-admin.form.select name="rule_group_id" :label="trans('model.rule_group.attribute')" :options="$ruleGroups" :placeholder="trans('common.none')" />
  33. <x-admin.form.input-group name="speed_limit" type="number" :label="trans('model.node.traffic_limit')" append="Mbps" required />
  34. <x-admin.form.input name="client_limit" type="number" :label="trans('model.node.client_limit')" required />
  35. <x-admin.form.input name="sort" type="number" :label="trans('model.common.sort')" required />
  36. <x-admin.form.select name="labels" :label="trans('model.node.label')" :options="$labels" multiple />
  37. <x-admin.form.select name="country_code" :label="trans('model.node.country')">
  38. @foreach ($countries as $country)
  39. <option data-icon="fi fis fi-{{ $country->code }}" value="{{ $country->code }}">
  40. {{ strtoupper($country->code) . ' - ' . $country->name }}
  41. </option>
  42. @endforeach
  43. </x-admin.form.select>
  44. <!-- 节点 细则部分 -->
  45. <x-admin.form.input-group name="next_renewal_date" attribute="data-plugin=datepicker" :label="trans('model.node.next_renewal_date')" />
  46. <x-admin.form.skeleton name="subscription_term_value" :label="trans('model.node.subscription_term')">
  47. <div class="input-group">
  48. <input class="form-control" id="subscription_term_value" type="number" min="1" />
  49. <select class="form-control" id="subscription_term_unit" data-plugin="selectpicker" data-style="btn-outline btn-primary">
  50. <option value="days">{{ ucfirst(trans('validation.attributes.day')) }}</option>
  51. <option value="months">{{ ucfirst(trans('validation.attributes.month')) }}</option>
  52. <option value="years">{{ ucfirst(trans('validation.attributes.year')) }}</option>
  53. </select>
  54. </div>
  55. </x-admin.form.skeleton>
  56. <x-admin.form.input name="renewal_cost" type="number" :label="trans('model.node.renewal_cost')" step="0.01" />
  57. <x-admin.form.textarea name="description" :label="trans('model.common.description')" />
  58. </div>
  59. <div class="col-lg-6">
  60. <h4 class="example-title">{{ trans('admin.node.info.extend') }}</h4>
  61. <x-admin.form.radio-group name="is_display" :label="trans('model.node.display')" :options="[
  62. 0 => trans('admin.node.info.display.invisible'),
  63. 1 => trans('admin.node.info.display.node'),
  64. 2 => trans('admin.node.info.display.sub'),
  65. 3 => trans('admin.node.info.display.all'),
  66. ]" :help="trans('admin.node.info.display.hint')" />
  67. <x-admin.form.radio-group name="detection_type" :label="trans('model.node.detection')" :options="[
  68. 0 => trans('common.close'),
  69. 1 => trans('admin.node.info.detection.tcp'),
  70. 2 => trans('admin.node.info.detection.icmp'),
  71. 3 => trans('admin.node.info.detection.all'),
  72. ]" :help="trans('admin.node.info.detection.hint')" />
  73. <!-- 中转 设置部分 -->
  74. <x-admin.form.select name="relay_node_id" :label="trans('model.node.transfer')" :options="$nodes" :placeholder="trans('common.none')" />
  75. <hr />
  76. <div class="relay-config">
  77. <x-admin.form.input name="port" type="number" :label="trans('model.node.relay_port')" />
  78. </div>
  79. <!-- 代理 设置部分 -->
  80. <div class="proxy-config">
  81. <x-admin.form.radio-group name="type" :label="trans('model.common.type')" :options="[0 => 'Shadowsocks', 1 => 'ShadowsocksR', 2 => 'V2Ray', 3 => 'Trojan', 4 => 'VNET']" />
  82. <hr />
  83. <!-- SS/SSR 设置部分 -->
  84. <div class="ss-setting">
  85. <x-admin.form.select name="method" :label="trans('model.node.method')" :options="$methods" />
  86. <!-- TODO: Supporting SS plugin -->
  87. {{-- <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']" /> --}}
  88. {{-- <x-admin.form.textarea name="plugin_opts" :label="trans('model.node.plugin_opts')" /> --}}
  89. <div class="ssr-setting">
  90. <x-admin.form.select name="protocol" :label="trans('model.node.protocol')" :options="$protocols" />
  91. <x-admin.form.textarea name="protocol_param" :label="trans('model.node.protocol_param')" />
  92. <x-admin.form.select name="obfs" :label="trans('model.node.obfs')" :options="$obfs" />
  93. <x-admin.form.textarea name="obfs_param" :label="trans('model.node.obfs_param')" :placeholder="trans('admin.node.info.obfs_param_hint')" />
  94. <x-admin.form.skeleton name="proxy_info" :label="trans('admin.node.proxy_info')">
  95. <div class="text-help">
  96. {!! trans('admin.node.proxy_info_hint') !!}
  97. </div>
  98. </x-admin.form.skeleton>
  99. </div>
  100. <hr />
  101. <x-admin.form.input name="single" type="checkbox" :label="trans('model.node.single')"
  102. attribute="data-plugin=switchery onchange=switchSetting('single')" :help="trans('admin.node.info.single_hint')" />
  103. <div class="single-setting">
  104. <x-admin.form.input name="port" type="number" :label="trans('model.node.service_port')" :help="trans('admin.node.info.single_hint')" />
  105. <x-admin.form.input name="passwd" :label="trans('model.node.single_passwd')" />
  106. </div>
  107. </div>
  108. <!-- V2ray TODO: Supporting new feature -->
  109. <div class="v2ray-setting">
  110. <x-admin.form.input name="v2_alter_id" :label="trans('model.node.v2_alter_id')" />
  111. <x-admin.form.input name="port" type="number" :label="trans('model.node.service_port')" />
  112. <x-admin.form.select name="v2_method" :label="trans('model.node.method')" :options="[
  113. 'none' => 'none',
  114. 'auto' => 'auto',
  115. 'zero' => 'zero',
  116. 'aes-128-gcm' => 'aes-128-gcm',
  117. 'chacha20-poly1305' => 'chacha20-poly1305',
  118. ]" :help="trans('admin.node.info.v2_method_hint')" />
  119. <x-admin.form.select name="v2_net" :label="trans('model.node.v2_net')" :options="[
  120. 'tcp' => 'TCP',
  121. 'kcp' => 'mKCP',
  122. 'ws' => 'WebSocket',
  123. 'httpupgrade' => 'HTTPUpgrade',
  124. 'xhttp' => 'xHTTP ',
  125. 'h2' => 'HTTP/2',
  126. 'quic' => 'QUIC',
  127. 'domainsocket' => 'DomainSocket',
  128. 'grpc' => 'gRPC',
  129. ]" :help="trans('admin.node.info.v2_net_hint')" />
  130. <x-admin.form.select name="v2_type" :label="trans('model.node.v2_cover')" :options="[
  131. 'none' => trans('admin.node.info.v2_cover.none'),
  132. 'http' => trans('admin.node.info.v2_cover.http'),
  133. 'srtp' => trans('admin.node.info.v2_cover.srtp'),
  134. 'utp' => trans('admin.node.info.v2_cover.utp'),
  135. 'wechat-video' => trans('admin.node.info.v2_cover.wechat'),
  136. 'dtls' => trans('admin.node.info.v2_cover.dtls'),
  137. 'wireguard' => trans('admin.node.info.v2_cover.wireguard'),
  138. ]" />
  139. <x-admin.form.input name="v2_host" :label="trans('model.node.v2_host')" :help="trans('admin.node.info.v2_host_hint')" />
  140. <x-admin.form.input name="v2_path" :label="trans('model.node.v2_path')" />
  141. <x-admin.form.input name="v2_sni" :label="trans('model.node.v2_sni')" />
  142. <x-admin.form.input name="v2_tls" type="checkbox" :label="trans('model.node.v2_tls')"
  143. attribute="data-plugin=switchery onchange=switchSetting('v2_tls')" />
  144. <x-admin.form.input name="tls_provider" :label="trans('model.node.v2_tls_provider')" :help="trans('admin.node.info.v2_tls_provider_hint')" />
  145. {{-- <x-admin.form.input name="mux" type="checkbox" :label="trans('model.node.mux')" attribute="data-plugin=switchery onchange=switchSetting('mux')" --}}
  146. {{-- :help="trans('admin.node.info.mux')" /> --}}
  147. </div>
  148. <!-- Trojan 设置部分 -->
  149. <div class="trojan-setting">
  150. <x-admin.form.input name="port" type="number" :label="trans('model.node.service_port')" />
  151. </div>
  152. </div>
  153. <x-admin.form.input name="is_udp" type="checkbox" :label="trans('model.node.udp')" attribute="data-plugin=switchery" />
  154. <x-admin.form.input name="status" type="checkbox" :label="trans('common.status.attribute')" attribute="data-plugin=switchery" />
  155. <div class="col-12 form-actions text-right">
  156. <a class="btn btn-secondary" href="{{ route('admin.node.index') }}">{{ trans('common.back') }}</a>
  157. <button class="btn btn-success" type="submit">{{ trans('common.submit') }}</button>
  158. </div>
  159. </div>
  160. </div>
  161. </x-admin.form.container>
  162. </x-ui.panel>
  163. </div>
  164. @endsection
  165. @section('javascript')
  166. <script src="/assets/global/vendor/bootstrap-select/bootstrap-select.min.js"></script>
  167. <script src="/assets/global/vendor/bootstrap-datepicker/bootstrap-datepicker.min.js"></script>
  168. @if (app()->getLocale() !== 'en')
  169. <script src="/assets/global/vendor/bootstrap-datepicker/locales/bootstrap-datepicker.{{ str_replace('_', '-', app()->getLocale()) }}.min.js" charset="UTF-8">
  170. </script>
  171. @endif
  172. <script src="/assets/global/js/Plugin/bootstrap-select.js"></script>
  173. <script src="/assets/global/js/Plugin/bootstrap-datepicker.js"></script>
  174. <script src="/assets/global/vendor/switchery/switchery.min.js"></script>
  175. <script src="/assets/global/js/Plugin/switchery.js"></script>
  176. <script>
  177. const string = "{{ strtolower(Str::random()) }}";
  178. function calculateNextNextRenewalDate() {
  179. const nextRenewalDate = $("#next_renewal_date").val();
  180. const termValue = parseInt($("#subscription_term_value").val() || 0);
  181. const termUnit = $("#subscription_term_unit").val();
  182. const nextNextRenewalDate = $("#next_next_renewal_date");
  183. if (!nextRenewalDate || termValue <= 0) {
  184. nextNextRenewalDate.val("");
  185. return;
  186. }
  187. const currentDate = new Date(nextRenewalDate);
  188. const originalDay = currentDate.getDate();
  189. if (termUnit === "months") {
  190. // 获取当前月份和年份
  191. let targetMonth = currentDate.getMonth() + termValue;
  192. let targetYear = currentDate.getFullYear() + Math.floor(targetMonth / 12);
  193. targetMonth = targetMonth % 12;
  194. // 先将日期设置为目标月的同一天
  195. currentDate.setFullYear(targetYear, targetMonth, originalDay);
  196. // 检查是否因月份天数不同而被自动调整
  197. if (currentDate.getMonth() !== targetMonth) {
  198. // 如果被调整,说明目标月份的天数比原始日期少
  199. // 将日期设置为目标月份的最后一天
  200. currentDate.setFullYear(targetYear, targetMonth + 1, 0);
  201. }
  202. } else {
  203. // 处理天数和年份的情况
  204. const adjustments = {
  205. days: "Date",
  206. years: "FullYear"
  207. };
  208. currentDate[`set${adjustments[termUnit]}`](
  209. currentDate[`get${adjustments[termUnit]}`]() + termValue
  210. );
  211. }
  212. // 显示计算结果(如果需要)
  213. if ($("#next_next_renewal_date").length) {
  214. nextNextRenewalDate.val(currentDate.toISOString().split("T")[0]);
  215. }
  216. }
  217. $(document).ready(function() {
  218. // 初始化UI元素
  219. initializeUI();
  220. // 绑定事件
  221. bindEvents();
  222. // 准备节点数据
  223. let nodeData = {
  224. is_ddns: 0,
  225. push_port: 1080,
  226. traffic_rate: 1.0,
  227. level: 0,
  228. speed_limit: 1000,
  229. client_limit: 1000,
  230. is_display: 3,
  231. detection_type: 0,
  232. is_udp: 1,
  233. status: 1,
  234. sort: 1,
  235. method: '{{ $methodDefault }}',
  236. protocol: '{{ $protocolDefault }}',
  237. obfs: '{{ $obfsDefault }}',
  238. relay_node_id: '',
  239. type: 1
  240. };
  241. @isset($node)
  242. // 反向解析节点数据以适配表单字段
  243. const node = @json($node);
  244. nodeData = {
  245. single: node.type === 0 || node.type === 1 || node.type === 4 ? (node.passwd ? 1 : 0) : undefined,
  246. ...node,
  247. v2_tls: node.type === 2 ? (node?.v2_tls === 'tls' ? 1 : 0) : undefined,
  248. };
  249. // 处理订阅期限字段
  250. if (node.subscription_term) {
  251. const [value, unit] = node.subscription_term.split(" ");
  252. nodeData.subscription_term_value = value;
  253. nodeData.subscription_term_unit = unit;
  254. }
  255. @endisset
  256. // 自动填充表单
  257. autoPopulateForm(nodeData);
  258. calculateNextNextRenewalDate();
  259. });
  260. function initializeUI() {
  261. $(".single-setting").hide();
  262. $("#v2_path").val("/" + string);
  263. }
  264. function bindEvents() {
  265. $("input:radio[name='type']").on("change", updateServiceType);
  266. $("#obfs").on("changed.bs.select", toggleObfsParam);
  267. $("#relay_node_id").on("changed.bs.select", toggleRelayConfig);
  268. $("#v2_net").on("changed.bs.select", updateV2RaySettings);
  269. $(document).on("change", "#next_renewal_date, #subscription_term_value, #subscription_term_unit", calculateNextNextRenewalDate);
  270. }
  271. function switchSetting(id) {
  272. const check = document.getElementById(id).checked;
  273. if (id === "single") {
  274. $(".single-setting").toggle(check);
  275. $("#single_port").attr({
  276. "hidden": !check,
  277. "required": check
  278. });
  279. if (!check) $("#passwd").val("");
  280. } else if (id === "is_ddns") {
  281. $("#ip, #ipv6").attr("readonly", check).val("");
  282. $("#server").attr("required", check);
  283. }
  284. }
  285. // 设置服务类型
  286. function updateServiceType() {
  287. const type = parseInt($(this).val());
  288. const settingsMap = {
  289. 0: [".ss-setting"],
  290. 1: [".ss-setting", ".ssr-setting"],
  291. 2: [".v2ray-setting", "#v2_port"],
  292. 3: [".trojan-setting", "#trojan_port"],
  293. 4: [".ss-setting", ".ssr-setting"]
  294. };
  295. $(".ss-setting, .ssr-setting, .v2ray-setting, .trojan-setting").hide();
  296. Object.keys(settingsMap).forEach(key => $(settingsMap[key].join(",")).hide());
  297. (settingsMap[type] || []).forEach(selector => $(selector).show());
  298. }
  299. function toggleObfsParam() {
  300. const $obfsParam = $("#obfs_param");
  301. const show = $("#obfs").val() !== "plain";
  302. $obfsParam.closest('.form-group').toggle(show);
  303. if (!show) $obfsParam.val("");
  304. }
  305. function toggleRelayConfig() {
  306. const hasRelay = $("#relay_node_id").val() !== "";
  307. $(".relay-config").toggle(hasRelay);
  308. $(".proxy-config").toggle(!hasRelay);
  309. $("#relay_port").attr({
  310. hidden: !hasRelay,
  311. required: hasRelay
  312. });
  313. }
  314. // 设置V2Ray详细设置
  315. function updateV2RaySettings() {
  316. const net = $(this).val();
  317. const $type = $(".v2_type");
  318. const $typeOption = $("#type_option");
  319. const $host = $(".v2_host");
  320. const $path = $("#v2_path");
  321. $type.show();
  322. $host.show();
  323. if (!$path.val()) {
  324. $path.val("/" + string);
  325. }
  326. switch (net) {
  327. case "ws":
  328. case "http":
  329. $type.hide();
  330. break;
  331. case "domainsocket":
  332. $type.hide();
  333. $host.hide();
  334. break;
  335. case "quic":
  336. $typeOption.attr("disabled", false);
  337. if (!$path.val()) {
  338. $path.val(string);
  339. }
  340. break;
  341. case "kcp":
  342. case "tcp":
  343. default:
  344. $typeOption.attr("disabled", true);
  345. break;
  346. }
  347. $("#v2_type").selectpicker("refresh");
  348. }
  349. // ajax同步提交
  350. function Submit() {
  351. // 收集表单数据
  352. const data = collectFormData('.form-horizontal');
  353. // 拼接 subscription_term
  354. const termValue = $("#subscription_term_value").val();
  355. const termUnit = $("#subscription_term_unit").val();
  356. data["subscription_term"] = termValue ? `${termValue} ${termUnit}` : null;
  357. // 发送 AJAX 请求
  358. ajaxRequest({
  359. url: '{{ isset($node) ? route('admin.node.update', $node['id']) : route('admin.node.store') }}',
  360. method: '{{ isset($node) ? 'PUT' : 'POST' }}',
  361. data: data,
  362. success: function(ret) {
  363. handleResponse(ret, {
  364. redirectUrl: '{{ route('admin.node.index') . (Request::getQueryString() ? '?' . Request::getQueryString() : '') }}'
  365. });
  366. },
  367. error: function(xhr) {
  368. handleErrors(xhr, {
  369. form: '.form-horizontal'
  370. });
  371. }
  372. });
  373. return false;
  374. }
  375. // 服务条款
  376. window.showTnc = function() {
  377. const jsonConfig = {
  378. "additional_ports": {
  379. "443": {
  380. "passwd": "ProxyPanel",
  381. "method": "none",
  382. "protocol": "auth_chain_a",
  383. "protocol_param": "#",
  384. "obfs": "plain",
  385. "obfs_param": "fe2.update.microsoft.com"
  386. }
  387. }
  388. };
  389. swal.fire({
  390. title: "[节点 user-config.json 配置示例]",
  391. width: "36em",
  392. html: `
  393. <div class="text-left">
  394. <ol>
  395. <li>请勿直接复制黏贴以下配置,SSR(R)会报错的</li>
  396. <li>确保服务器时间为CST</li>
  397. </ol>
  398. <pre class="bg-grey-800 text-white">${JSON.stringify(jsonConfig, null, 2)}</pre>
  399. </div>
  400. `,
  401. icon: "info"
  402. });
  403. };
  404. // 模式提示
  405. window.showPortsOnlyConfig = function() {
  406. swal.fire({
  407. title: "[节点 user-config.json 配置示例]",
  408. width: "36em",
  409. html: `
  410. <ul class="bg-grey-800 text-white text-left">
  411. <li>严格模式:"additional_ports_only": "true"</li>
  412. <li>兼容模式:"additional_ports_only": "false"</li>
  413. </ul>
  414. `,
  415. icon: "info"
  416. });
  417. };
  418. </script>
  419. @endsection