index.blade.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. @extends('admin.table_layouts')
  2. @push('css')
  3. <link href="/assets/custom/range.min.css" rel="stylesheet">
  4. @endpush
  5. @section('content')
  6. <div class="page-content container-fluid">
  7. <x-admin.table-panel :title="trans('admin.menu.user.list')" :theads="[
  8. 'id' => '#',
  9. trans('model.user.username'),
  10. 'credit' => trans('model.user.credit'),
  11. 'port' => trans('model.user.port'),
  12. trans('model.subscribe.code'),
  13. trans('model.user.traffic_used'),
  14. 't' => trans('common.latest_at'),
  15. 'expired_at' => trans('common.expired_at'),
  16. trans('common.account'),
  17. trans('model.user.service'),
  18. trans('common.action'),
  19. ]" :count="trans('admin.user.counts', ['num' => $userList->total()])" :pagination="$userList->links()" :delete-config="['url' => route('admin.user.destroy', 'PLACEHOLDER'), 'attribute' => trans('model.user.attribute')]">
  20. @canany(['admin.user.batch', 'admin.user.create'])
  21. <x-slot:actions>
  22. @can('admin.user.batch')
  23. <button class="btn btn-outline-default" onclick="batchAddUsers()">
  24. <i class="icon wb-plus" aria-hidden="true"></i> {{ trans('common.generate') }}
  25. </button>
  26. @endcan
  27. @can('admin.user.create')
  28. <a class="btn btn-outline-primary" href="{{ route('admin.user.create') }}">
  29. <i class="icon wb-user-add" aria-hidden="true"></i> {{ trans('common.add') }}
  30. </a>
  31. @endcan
  32. </x-slot:actions>
  33. @endcanany
  34. <x-slot:filters>
  35. <x-admin.filter.input class="col-md-1 col-sm-4" name="id" type="number" :placeholder="trans('model.user.id')" />
  36. <x-admin.filter.input class="col-xxl-2 col-md-3 col-sm-4" name="username" :placeholder="trans('model.user.username')" />
  37. <x-admin.filter.input class="col-xxl-2 col-md-3 col-sm-4" name="wechat" :placeholder="trans('model.user.wechat')" />
  38. <x-admin.filter.input class="col-xxl-2 col-md-3 col-sm-4" name="qq" type="number" :placeholder="trans('model.user.qq')" />
  39. <x-admin.filter.input class="col-xxl-1 col-md-2 col-sm-4" name="port" type="number" :placeholder="trans('model.user.port')" />
  40. <x-admin.filter.selectpicker class="col-xxl-1 col-md-3 col-4" name="user_group_id" :title="trans('model.user_group.attribute')" :options="$userGroups" />
  41. <x-admin.filter.selectpicker class="col-xxl-1 col-md-3 col-4" name="level" :title="trans('model.common.level')" :options="$levels" />
  42. <x-admin.filter.selectpicker class="col-xxl-1 col-md-3 col-4" name="status" :title="trans('model.user.account_status')" :options="[-1 => trans('common.status.banned'), 0 => trans('common.status.inactive'), 1 => trans('common.status.normal')]" />
  43. <x-admin.filter.selectpicker class="col-xxl-1 col-md-3 col-4" name="enable" :title="trans('model.user.proxy_status')" :options="[1 => trans('common.status.enabled'), 0 => trans('common.status.banned')]" />
  44. </x-slot:filters>
  45. <x-slot:tbody>
  46. @foreach ($userList as $user)
  47. <tr class="{{ $user->ban_time ? 'table-danger' : '' }}">
  48. <td> {{ $user->id }} </td>
  49. <td> {{ $user->username }} </td>
  50. <td> {{ $user->credit }} </td>
  51. <td>
  52. {!! $user->port ?: '<span class="badge badge-lg badge-danger"> ' . trans('common.none') . ' </span>' !!}
  53. </td>
  54. <td>
  55. <a class="copySubscribeLink" data-clipboard-text="{{ $user->sub_url }}" href="javascript:">{{ $user->subscribe->code }}</a>
  56. </td>
  57. <td> {{ formatBytes($user->used_traffic) }} / {{ $user->transfer_enable_formatted }} </td>
  58. <td> {{ $user->t ? date('Y-m-d H:i', $user->t) : trans('common.status.unused') }} </td>
  59. <td>
  60. @if ($user->expiration_status !== 3)
  61. <span class="badge badge-lg badge-{{ ['danger', 'warning', 'default'][$user->expiration_status] }}">
  62. {{ $user->expiration_date }} </span>
  63. @else
  64. {{ $user->expiration_date }}
  65. @endif
  66. </td>
  67. <td>
  68. @if ($user->status > 0)
  69. <span class="badge badge-lg badge-primary">
  70. <i class="wb-check" aria-hidden="true"></i>
  71. </span>
  72. @elseif ($user->status < 0)
  73. <span class="badge badge-lg badge-danger">
  74. <i class="wb-close" aria-hidden="true"></i>
  75. </span>
  76. @else
  77. <span class="badge badge-lg badge-default">
  78. <i class="wb-minus" aria-hidden="true"></i>
  79. </span>
  80. @endif
  81. </td>
  82. <td>
  83. <span class="badge badge-lg badge-{{ $user->enable ? 'info' : 'danger' }}">
  84. <i class="wb-{{ $user->enable ? 'check' : 'close' }}" aria-hidden="true"></i>
  85. </span>
  86. </td>
  87. <td>
  88. @canany(['admin.user.edit', 'admin.user.destroy', 'admin.user.export', 'admin.user.monitor', 'admin.user.online', 'admin.user.reset',
  89. 'admin.user.switch'])
  90. <x-ui.dropdown>
  91. @can('admin.user.edit')
  92. <x-ui.dropdown-item :url="route('admin.user.edit', ['user' => $user->id, Request::getQueryString()])" icon="wb-edit" :text="trans('common.edit')" />
  93. @endcan
  94. @can('admin.user.destroy')
  95. <x-ui.dropdown-item color="red-600" url="javascript:(0)" attribute="data-action=delete" icon="wb-trash" :text="trans('common.delete')" />
  96. @endcan
  97. @can('admin.user.export')
  98. <x-ui.dropdown-item :url="route('admin.user.export', $user)" icon="wb-code" :text="trans('admin.user.proxy_info')" />
  99. @endcan
  100. @can('admin.user.monitor')
  101. <x-ui.dropdown-item :url="route('admin.user.monitor', $user)" icon="wb-stats-bars" :text="trans('admin.user.traffic_monitor')" />
  102. @endcan
  103. @can('admin.user.online')
  104. <x-ui.dropdown-item :url="route('admin.user.online', $user)" icon="wb-cloud" :text="trans('admin.user.online_monitor')" />
  105. @endcan
  106. @can('admin.user.reset')
  107. <x-ui.dropdown-item url="javascript:resetTraffic('{{ $user->id }}','{{ $user->username }}')" icon="wb-reload"
  108. :text="trans('admin.user.reset_traffic')" />
  109. @endcan
  110. @can('admin.user.switch')
  111. <x-ui.dropdown-item url="javascript:switchToUser('{{ $user->id }}')" icon="wb-user" :text="trans('admin.user.user_view')" />
  112. @endcan
  113. @can('admin.user.VNetInfo')
  114. <x-ui.dropdown-item id="vent_{{ $user->id }}" url="javascript:VNetInfo('{{ $user->id }}')" icon="wb-link-broken"
  115. :text="trans('admin.user.connection_test')" />
  116. @endcan
  117. </x-ui.dropdown>
  118. @endcanany
  119. </td>
  120. </tr>
  121. @endforeach
  122. </x-slot:tbody>
  123. </x-admin.table-panel>
  124. </div>
  125. <!-- 用户VNet检测结果模态框 -->
  126. <x-ui.modal id="userVNetCheckModal" :title="trans('admin.user.connection_test')" size="lg">
  127. </x-ui.modal>
  128. @endsection
  129. @push('javascript')
  130. @vite(['resources/js/app.js'])
  131. <script>
  132. window.i18n.extend({
  133. 'broadcast': {
  134. 'error': '{{ trans('common.error') }}',
  135. 'websocket_unavailable': '{{ trans('common.broadcast.websocket_unavailable') }}',
  136. 'websocket_disconnected': '{{ trans('common.broadcast.websocket_disconnected') }}',
  137. 'setup_failed': '{{ trans('common.broadcast.setup_failed') }}',
  138. 'disconnect_failed': '{{ trans('common.broadcast.disconnect_failed') }}'
  139. }
  140. });
  141. @can('admin.user.batch')
  142. function batchAddUsers() { // 批量生成账号
  143. showConfirm({
  144. title: '{{ trans('admin.user.bulk_account_quantity') }}',
  145. input: "range",
  146. inputAttributes: {
  147. min: 1,
  148. max: 10
  149. },
  150. inputValue: 1,
  151. onConfirm: function(result) {
  152. if (result.value) {
  153. ajaxPost('{{ route('admin.user.batch') }}', {
  154. amount: result.value
  155. });
  156. }
  157. }
  158. });
  159. }
  160. @endcan
  161. @can('admin.user.reset')
  162. function resetTraffic(id, username) { // 重置流量
  163. showConfirm({
  164. title: '{{ trans('common.warning') }}',
  165. text: '{{ trans('admin.user.reset_confirm') }}'.replace('{username}', username),
  166. icon: 'warning',
  167. onConfirm: function() {
  168. ajaxPost(jsRoute('{{ route('admin.user.reset', 'PLACEHOLDER') }}', id));
  169. }
  170. });
  171. }
  172. @endcan
  173. @can('admin.user.switch')
  174. function switchToUser(id) { // 切换用户身份
  175. ajaxPost(jsRoute('{{ route('admin.user.switch', 'PLACEHOLDER') }}', id), {}, {
  176. success: function(ret) {
  177. handleResponse(ret, {
  178. redirectUrl: '/'
  179. });
  180. }
  181. });
  182. }
  183. @endcan
  184. @can('admin.user.VNetInfo')
  185. // 全局状态
  186. const userVNetState = {
  187. results: {} // 按 nodeId 存储节点信息与已收到的数据
  188. };
  189. // 构建并显示模态框
  190. function buildVNetCheckUI() {
  191. const body = document.querySelector('#userVNetCheckModal .modal-body');
  192. let html = `<table class="table table-hover">
  193. <thead>
  194. <tr>
  195. <th>{{ trans('model.node.attribute') }}</th>
  196. <th>{{ trans('common.status.attribute') }}</th>
  197. </tr>
  198. </thead>
  199. <tbody>`;
  200. Object.keys(userVNetState.results).forEach(nodeId => {
  201. const node = userVNetState.results[nodeId];
  202. html += `
  203. <tr data-node-id="${nodeId}">
  204. <td>${node.name}</td>
  205. <td><i class="wb-loop icon-spin"></i></td>
  206. </tr>`;
  207. });
  208. html += '</tbody></table></div>';
  209. body.innerHTML = html;
  210. }
  211. // 更新模态框中的节点状态
  212. function updateVNetCheckUI(nodeId, available) {
  213. try {
  214. const row = document.querySelector(`#userVNetCheckModal tr[data-node-id="${nodeId}"]`);
  215. if (!row) return;
  216. const statusEl = row.querySelector('td:nth-child(2)');
  217. if (statusEl) {
  218. statusEl.innerHTML = available ? '✔️' : '❌';
  219. }
  220. } catch (e) {}
  221. }
  222. // 处理广播数据
  223. function handleVNetResult(e) {
  224. // 如果包含 nodeList:构建初始 UI 框架
  225. if (e.data && e.data.nodeList) {
  226. $('#userVNetCheckModal').modal('show');
  227. userVNetState.results = {};
  228. Object.keys(e.data.nodeList).forEach(nodeId => {
  229. const nodeName = e.data.nodeList[nodeId];
  230. userVNetState.results[nodeId] = {
  231. name: nodeName,
  232. available: null // 检查中
  233. };
  234. });
  235. // 构建并显示 modal
  236. buildVNetCheckUI();
  237. return;
  238. }
  239. // 处理详细数据
  240. try {
  241. const nodeId = e.data.nodeId;
  242. if (!nodeId || !userVNetState.results[nodeId]) return;
  243. userVNetState.results[nodeId].available = e.data.available;
  244. updateVNetCheckUI(nodeId, e.data.available);
  245. } catch (err) {
  246. console.error('handleVNetResult error', err);
  247. }
  248. }
  249. function VNetInfo(id) { // 节点连通性测试
  250. const $triggerElement = $(`#vent_${id}`);
  251. const channelName = window.broadcastingManager.getChannelName('user.check', id);
  252. // 清理之前的连接
  253. window.broadcastingManager.unsubscribe(channelName);
  254. userVNetState.results = {};
  255. // 启动 spinner
  256. $triggerElement.removeClass("wb-link-broken").addClass("wb-loop icon-spin");
  257. // 使用统一的广播管理器订阅频道
  258. const success = window.broadcastingManager.subscribe(
  259. channelName,
  260. '.user.vnet.tasks',
  261. (e) => handleVNetResult(e)
  262. );
  263. if (!success) {
  264. // 订阅失败:恢复按钮状态
  265. $triggerElement.removeClass("wb-loop icon-spin").addClass("wb-link-broken");
  266. return;
  267. }
  268. // 触发后端接口(Ajax)
  269. ajaxPost(jsRoute('{{ route('admin.user.VNetInfo', 'PLACEHOLDER') }}', id), {}, {
  270. beforeSend: function() {
  271. // spinner 已经设置
  272. },
  273. success: function(ret) {
  274. // 不在此处处理最终结果,交由广播处理(避免 race)
  275. },
  276. error: function(xhr, status, error) {
  277. window.broadcastingManager.handleAjaxError(
  278. '{{ trans('common.error') }}',
  279. `{{ trans('common.request_failed') }} ${error}: ${xhr?.responseJSON?.exception}`
  280. );
  281. // 出错时恢复 spinner
  282. $triggerElement.removeClass("wb-loop icon-spin").addClass("wb-link-broken");
  283. },
  284. complete: function() {
  285. // 不在这里恢复按钮状态,而是等所有广播完成后再恢复
  286. }
  287. });
  288. }
  289. @endcan
  290. $(document).on('click', '.copySubscribeLink', function(e) {
  291. e.preventDefault();
  292. copyToClipboard($(this).data('clipboard-text'));
  293. });
  294. </script>
  295. @endpush