Просмотр исходного кода

Improve charts and pages layout display in Mobile

BrettonYe 1 неделя назад
Родитель
Сommit
2d9088bf6b

+ 1 - 1
app/Http/Controllers/User/NodeController.php

@@ -24,7 +24,7 @@ class NodeController extends Controller
         // 提取节点地理位置信息用于地图显示
         $nodesGeo = $nodes->pluck('name', 'geo');
 
-        return view('user.nodeList', compact('nodesGeo', 'nodes'));
+        return view('user.services', compact('nodesGeo', 'nodes'));
     }
 
     public function show(Request $request, Node $node, ProxyService $proxyServer): JsonResponse

+ 1 - 1
app/Http/Controllers/User/ShopController.php

@@ -41,7 +41,7 @@ class ShopController extends Controller
         // 计算数据增加天数
         $dataPlusDays = $user->reset_time ?? $user->expired_at;
 
-        return view('user.services', [
+        return view('user.shop', [
             'chargeGoodsList' => Goods::type(3)->orderBy('price')->get(),
             'goodsList' => $goodsList,
             'renewTraffic' => $renewPrice ? Helpers::getPriceTag($renewPrice) : 0,

Разница между файлами не показана из-за своего большого размера
+ 0 - 6
public/assets/global/vendor/chart-js/chart.min.js


Разница между файлами не показана из-за своего большого размера
+ 6 - 0
public/assets/global/vendor/chart-js/chart.umd.min.js


+ 1 - 1
resources/views/admin/logs/userMonitor.blade.php

@@ -20,7 +20,7 @@
     </div>
 @endsection
 @section('javascript')
-    <script src="/assets/global/vendor/chart-js/chart.min.js"></script>
+    <script src="/assets/global/vendor/chart-js/chart.umd.min.js"></script>
     <script>
         function common_options(tail) {
             return {

+ 1 - 1
resources/views/admin/node/monitor.blade.php

@@ -22,7 +22,7 @@
     </div>
 @endsection
 @section('javascript')
-    <script src="/assets/global/vendor/chart-js/chart.min.js"></script>
+    <script src="/assets/global/vendor/chart-js/chart.umd.min.js"></script>
     <script>
         function common_options(tail) {
             return {

+ 1 - 1
resources/views/admin/report/accounting.blade.php

@@ -34,7 +34,7 @@
     </div>
 @endsection
 @section('javascript')
-    <script src="/assets/global/vendor/chart-js/chart.min.js"></script>
+    <script src="/assets/global/vendor/chart-js/chart.umd.min.js"></script>
     <script type="text/javascript">
         function label_callbacks(tail) {
             return {

+ 1 - 1
resources/views/admin/report/nodeDataAnalysis.blade.php

@@ -84,7 +84,7 @@
     </div>
 @endsection
 @section('javascript')
-    <script src="/assets/global/vendor/chart-js/chart.min.js"></script>
+    <script src="/assets/global/vendor/chart-js/chart.umd.min.js"></script>
     <script src="/assets/global/vendor/chart-js/chartjs-plugin-datalabels.min.js"></script>
     <script src="/assets/global/vendor/bootstrap-select/bootstrap-select.min.js"></script>
     <script src="/assets/global/js/Plugin/bootstrap-select.js"></script>

+ 1 - 1
resources/views/admin/report/siteDataAnalysis.blade.php

@@ -74,7 +74,7 @@
     </div>
 @endsection
 @section('javascript')
-    <script src="/assets/global/vendor/chart-js/chart.min.js"></script>
+    <script src="/assets/global/vendor/chart-js/chart.umd.min.js"></script>
     <script src="/assets/global/vendor/bootstrap-select/bootstrap-select.min.js"></script>
     <script src="/assets/global/js/Plugin/bootstrap-select.js"></script>
     <script type="text/javascript">

+ 42 - 23
resources/views/admin/report/userDataAnalysis.blade.php

@@ -19,7 +19,7 @@
         </div>
         @if (count($data) > 2)
             <div class="card card-shadow">
-                <div class="card-block p-30">
+                <div class="card-block p-lg-30 p-10">
                     <div class="row pb-20">
                         <div class="col-md-4 col-sm-6">
                             <div class="blue-grey-700 font-size-26 font-weight-500">{{ trans('admin.report.hourly_traffic') }}</div>
@@ -41,7 +41,7 @@
                 </div>
             </div>
             <div class="card card-shadow">
-                <div class="card-block p-30">
+                <div class="card-block p-lg-30 p-10">
                     <div class="row pb-20">
                         <div class="col-md-4 col-sm-6">
                             <div class="blue-grey-700 font-size-26 font-weight-500">{{ trans('admin.report.daily_traffic') }}</div>
@@ -75,7 +75,7 @@
     </div>
 @endsection
 @section('javascript')
-    <script src="/assets/global/vendor/chart-js/chart.min.js"></script>
+    <script src="/assets/global/vendor/chart-js/chart.umd.min.js"></script>
     <script src="/assets/global/vendor/chart-js/chartjs-plugin-datalabels.min.js"></script>
     <script src="/assets/global/vendor/bootstrap-select/bootstrap-select.min.js"></script>
     <script src="/assets/global/js/Plugin/bootstrap-select.js"></script>
@@ -146,6 +146,7 @@
 
         const createBarChart = (elementId, labels, datasets, labelTail, unit = "MiB") => {
             const optimizedDatasets = optimizeDatasets(datasets);
+            const isMobile = window.innerWidth <= 768;
             new Chart(document.getElementById(elementId), {
                 type: "bar",
                 data: {
@@ -154,23 +155,35 @@
                 },
                 plugins: [ChartDataLabels],
                 options: {
+                    aspectRatio: isMobile ? 0.8 : 2,
                     parsing: {
                         xAxisKey: "time",
                         yAxisKey: "total"
                     },
                     scales: {
                         x: {
-                            stacked: true
+                            stacked: true,
+                            ticks: {
+                                font: {
+                                    size: isMobile ? 10 : 12,
+                                },
+                            }
                         },
                         y: {
-                            stacked: true
+                            stacked: true,
+                            grace: '10%', // 在 Y 轴最大值上方自动留出 20% 空间,防止数值贴边
+                            ticks: {
+                                font: {
+                                    size: isMobile ? 10 : 12,
+                                }
+                            }
                         }
                     },
-                    responsive: true,
                     plugins: {
                         legend: {
+                            display: !isMobile, // 移动端隐藏图例
                             labels: {
-                                padding: 10,
+                                padding: 15,
                                 usePointStyle: true,
                                 pointStyle: "circle",
                                 font: {
@@ -180,27 +193,33 @@
                         },
                         tooltip: {
                             mode: "index",
-                            intersect: false,
+                            intersect: true,
+                            filter: (item) => item.raw.total > 0.01,
+                            itemSort: (a, b) => b.raw.total - a.raw.total,
+                            bodyFont: {
+                                size: isMobile ? 10 : 14,
+                            },
                             callbacks: {
-                                title: context => `${context[0].label} ${labelTail}`,
-                                label: context => {
-                                    const dataset = context.dataset;
-                                    const value = dataset.data[context.dataIndex]?.total || context.parsed.y;
-                                    return `${dataset.label || ""}: ${value.toFixed(2)} ${unit}`;
-                                }
+                                label: (ctx) => ` ${ctx.dataset.label}: ${ctx.raw.total.toFixed(2)} ${unit}`
                             }
                         },
                         datalabels: {
-                            display: true,
-                            align: "end",
+                            display: (ctx) => {
+                                // 仅顶层显示 & 数值大于0
+                                if (ctx.datasetIndex !== ctx.chart.data.datasets.length - 1) return false;
+                                const total = ctx.chart.data.datasets.reduce((sum, ds) =>
+                                    sum + (ds.data[ctx.dataIndex]?.total || 0), 0);
+                                return total > 0;
+                            },
+                            align: "top",
                             anchor: "end",
-                            formatter: (value, context) => {
-                                if (context.datasetIndex === context.chart.data.datasets.length - 1) {
-                                    let total = context.chart.data.datasets.reduce((sum, dataset) => sum + dataset.data[context.dataIndex].total,
-                                        0);
-                                    return total.toFixed(2) + unit;
-                                }
-                                return null;
+                            font: {
+                                size: isMobile ? 10 : 14,
+                            },
+                            formatter: (value, ctx) => {
+                                const total = ctx.chart.data.datasets.reduce((sum, ds) =>
+                                    sum + (ds.data[ctx.dataIndex]?.total || 0), 0);
+                                return total.toFixed(1) + unit;
                             }
                         }
                     }

+ 97 - 39
resources/views/user/index.blade.php

@@ -3,14 +3,28 @@
     <link href="/assets/global/vendor/bootstrap-select/bootstrap-select.min.css" rel="stylesheet">
     <link href="/assets/global/fonts/font-awesome/css/all.min.css" rel="stylesheet">
     <link href="/assets/global/vendor/aspieprogress/asPieProgress.min.css" rel="stylesheet">
+    <style>
+        .panel-info .announcement-content {
+            overflow-y: auto !important;
+        }
+
+        .pie-container {
+            width: var(--min-size, 100px);
+            margin: 0 auto;
+        }
+
+        .list-group-dividered .list-group-item:last-child {
+            border-bottom: none;
+        }
+    </style>
 @endsection
 @section('content')
     <div class="page-content container-fluid">
-        <div class="row" data-plugin="matchHeight" data-by-row="true">
+        <div class="row">
             @if (Session::has('successMsg'))
                 <x-alert class="col-md-12" :message="Session::pull('successMsg')" />
             @endif
-            <div class="col-xxl-3 col-xl-4 col-lg-5 col-md-6 col-12">
+            <div class="col-xxl-3 col-xl-4 col-lg-5 col-12">
                 <div class="card card-shadow">
                     <div class="card-block p-20">
                         <button class="btn btn-floating btn-sm btn-pure" type="button">
@@ -62,7 +76,7 @@
                 <div class="card card-shadow">
                     <div class="card-block p-20">
                         <div class="row">
-                            <div class="col-lg-7 col-md-12 col-sm-7">
+                            <div class="col-sm-7">
                                 <button class="btn btn-floating btn-sm btn-pure" type="button">
                                     <i class="wb-stats-bars cyan-500"></i>
                                 </button>
@@ -84,10 +98,13 @@
                                     @endif
                                 </div>
                             </div>
-                            <div class="col-lg-5 col-md-12 col-sm-5">
-                                <div class="w-only-xs-p50 w-only-sm-p75 w-only-md-p50" data-plugin="pieProgress" data-valuemax="100" data-barcolor="#96A3FA"
-                                     data-size="100" data-barsize="10" data-goal="{{ $unusedPercent }}" role="progressbar" aria-valuenow="{{ $unusedPercent }}">
-                                    <span class="pie-progress-number blue-grey-700 font-size-20">{{ $unusedPercent }}%</span>
+
+                            <div class="col-sm-5 d-flex align-items-center justify-content-center">
+                                <div class="pie-container">
+                                    <div data-plugin="pieProgress" data-valuemax="100" data-barcolor="#96A3FA" data-size="100" data-barsize="10"
+                                         data-goal="{{ $unusedPercent }}" role="progressbar" aria-valuenow="{{ $unusedPercent }}">
+                                        <span class="pie-progress-number blue-grey-700 font-size-20">{{ $unusedPercent }}%</span>
+                                    </div>
                                 </div>
                             </div>
                         </div>
@@ -118,27 +135,31 @@
                                 <i class="wb-globe purple-500"></i>
                             </button>
                             <span class="font-weight-400 mb-10">{{ trans('user.account.last_login') }}</span>
-                            <ul class="list-group list-group-dividered px-20 mb-0">
-                                <li class="list-group-item px-0">
-                                    <i class="icon wb-time"></i>{{ ucfirst(trans('validation.attributes.time')) }}:
-                                    {{ date_format($userLoginLog->created_at, 'Y/m/d H:i') }}
+                            <ul class="list-group list-group-dividered">
+                                <li class="list-group-item">
+                                    <i class="icon wb-time"></i> {{ ucfirst(trans('validation.attributes.time')) }}
+                                    <span class="float-right">{{ date_format($userLoginLog->created_at, 'Y/m/d H:i') }}</span>
                                 </li>
-                                <li class="list-group-item px-0">
-                                    <i class="icon wb-code"></i>{{ trans('user.attribute.ip') }}: {{ $userLoginLog->ip }}
+                                <li class="list-group-item">
+                                    <i class="icon wb-code"></i> {{ trans('user.attribute.ip') }}
+                                    <span class="float-right">{{ $userLoginLog->ip }}</span>
                                 </li>
-                                <li class="list-group-item px-0">
-                                    <i class="icon wb-cloud"></i>{{ trans('user.attribute.isp') }}: {{ $userLoginLog->isp }}
+                                <li class="list-group-item">
+                                    <i class="icon wb-cloud"></i> {{ trans('user.attribute.isp') }}
+                                    <span class="float-right">{{ $userLoginLog->isp }}</span>
                                 </li>
-                                <li class="list-group-item px-0">
-                                    <i class="icon wb-map"></i>{{ trans('user.attribute.address') }}:
-                                    {{ $userLoginLog->country . ' ' . $userLoginLog->province . ' ' . $userLoginLog->city . ' ' . $userLoginLog->area }}
+                                <li class="list-group-item">
+                                    <i class="icon wb-map"></i> {{ trans('user.attribute.address') }}
+                                    <span class="float-right">
+                                        {{ $userLoginLog->country . ' ' . $userLoginLog->province . ' ' . $userLoginLog->city . ' ' . $userLoginLog->area }}
+                                    </span>
                                 </li>
                             </ul>
                         </div>
                     </div>
                 @endif
             </div>
-            <div class="col-xxl-9 col-xl-8 col-lg-7 col-md-6 col-12">
+            <div class="col-xxl-9 col-xl-8 col-lg-7 col-12">
                 <div class="row" data-plugin="matchHeight" data-by-row="true">
                     <div class="col-xl-4 mb-30">
                         <div class="card card-shadow h-full">
@@ -206,8 +227,10 @@
                             <div class="card-block text-center">
                                 <i class="font-size-40 wb-wrench"></i>
                                 <h4 class="card-title">{{ trans('user.clients') }}</h4>
-                                <p class="card-text">{{ trans('common.download') . ' & ' . trans('user.tutorials') }}</p>
-                                <a class="btn btn-primary mb-10" href="{{ route('knowledge.index') }}">{{ trans('common.goto') }}</a>
+                                <div class="card-body">
+                                    <p>{{ trans('common.download') . ' & ' . trans('user.tutorials') }}</p>
+                                    <a class="btn btn-primary mb-10" href="{{ route('knowledge.index') }}">{{ trans('common.goto') }}</a>
+                                </div>
                             </div>
                         </div>
                     </div>
@@ -243,8 +266,8 @@
                         </div>
                     @endif
                 </div>
-                <div class="row" data-plugin="matchHeight" data-by-row="true">
-                    <div class="col-xxl-6 mb-30">
+                <div class="row">
+                    <div class="col-xxl-6 mb-30 mb-xl-0">
                         <div class="panel panel-info panel-line h-full">
                             <div class="panel-heading">
                                 <h2 class="panel-title">
@@ -255,20 +278,17 @@
                                     {{ $announcements->links() }}
                                 </div>
                             </div>
-                            <div class="panel-body" data-show-on-hover="false" data-direction="vertical" data-skin="scrollable-shadow"
-                                 data-plugin="scrollable">
-                                <div data-role="container">
-                                    <div class="pb-10" data-role="content">
-                                        @forelse($announcements as $announcement)
-                                            <h2 class="text-center">{!! $announcement->title !!}</h2>
-                                            <p class="text-right"><small>{{ trans('common.updated_at') }}
-                                                    <code>{{ $announcement->updated_at }}</code></small></p>
-                                            {!! $announcement->content !!}
-                                        @empty
-                                            <p class="text-center font-size-40">{{ trans('user.home.empty_announcement') }}</p>
-                                        @endforelse
+                            <div class="panel-body">
+                                @forelse($announcements as $announcement)
+                                    <h2 class="text-center">{!! $announcement->title !!}</h2>
+                                    <p class="text-right"><small>{{ trans('common.updated_at') }}
+                                            <code>{{ $announcement->updated_at }}</code></small></p>
+                                    <div class="announcement-content">
+                                        {!! $announcement->content !!}
                                     </div>
-                                </div>
+                                @empty
+                                    <p class="text-center font-size-40">{{ trans('user.home.empty_announcement') }}</p>
+                                @endforelse
                             </div>
                         </div>
                     </div>
@@ -312,7 +332,7 @@
 @section('javascript')
     <script src="/assets/global/vendor/aspieprogress/jquery-asPieProgress.min.js"></script>
     <script src="/assets/global/vendor/matchheight/jquery.matchHeight-min.js"></script>
-    <script src="/assets/global/vendor/chart-js/chart.min.js"></script>
+    <script src="/assets/global/vendor/chart-js/chart.umd.min.js"></script>
     <script src="/assets/global/vendor/bootstrap-select/bootstrap-select.min.js"></script>
     <script src="/assets/global/js/Plugin/aspieprogress.js"></script>
     <script src="/assets/global/js/Plugin/matchheight.js"></script>
@@ -412,13 +432,13 @@
             };
         }
 
-        new Chart(document.getElementById("dailyChart"), {
+        let dailyChartInstance = new Chart(document.getElementById("dailyChart"), {
             type: "line",
             data: datasets(@json($dayHours), @json($trafficHourly)),
             options: common_options(@json(trans_choice('common.hour', 2)))
         });
 
-        new Chart(document.getElementById("monthlyChart"), {
+        let monthlyChartInstance = new Chart(document.getElementById("monthlyChart"), {
             type: "line",
             data: datasets(@json($monthDays), @json($trafficDaily)),
             options: common_options(@json(trans_choice('common.days.attribute', 2)))
@@ -460,5 +480,43 @@
                 window.location.reload();
             }
         }
+
+        function syncAnnouncementHeight() {
+            // 获取右侧面板的总高度
+            const rightPanelHeight = $(".panel-primary .panel-body").outerHeight();
+            if (rightPanelHeight > 0) {
+                $('.panel-info .announcement-content').css({
+                    'max-height': rightPanelHeight - 20 + 'px',
+                });
+            }
+        }
+
+        function syncPieSize() {
+            const $container = $(".pie-container");
+            const $parent = $container.parent();
+            if ($parent.length) {
+                let minSize = Math.min($parent.width(), $parent.height());
+                $container.css('--min-size', minSize + 'px');
+
+                // 仅在必要时重绘插件,避免动画闪烁
+                const $pie = $('[data-plugin="pieProgress"]');
+                if ($pie.data('asPieProgress')) {
+                    $pie.asPieProgress('update');
+                }
+            }
+        }
+
+        $(document).ready(function() {
+            syncAnnouncementHeight();
+            syncPieSize();
+        });
+
+        // 窗口缩放时重新计算
+        $(window).on('resize', function() {
+            if (dailyChartInstance) dailyChartInstance.resize();
+            if (monthlyChartInstance) monthlyChartInstance.resize();
+            syncPieSize();
+            syncAnnouncementHeight();
+        });
     </script>
 @endsection

+ 2 - 2
resources/views/user/knowledge.blade.php

@@ -11,7 +11,7 @@
     <div class="page-content container-fluid">
         @if ($knowledge->isNotEmpty())
             <div class="row">
-                <div class="offset-xxl-1 col-xxl-2 col-xl-3 offset-lg-0 col-lg-4 offset-sm-2 col-sm-8">
+                <div class="offset-xxl-1 col-xxl-2 col-xl-3 col-lg-4 col-12">
                     <div class="panel">
                         <div class="list-group" role="tablist">
                             @foreach ($knowledge as $category => $articles)
@@ -22,7 +22,7 @@
                         </div>
                     </div>
                 </div>
-                <div class="col-xxl-8 col-xl-9 col-lg-8 col-md-12">
+                <div class="col-xxl-8 col-xl-9 col-lg-8 col-12">
                     <div class="panel">
                         <div class="panel-heading progress" id="loading_article" style="display: none;">
                             <div class="progress-bar progress-bar-striped active" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100"

+ 0 - 244
resources/views/user/nodeList.blade.php

@@ -1,244 +0,0 @@
-@extends('user.layouts')
-@section('css')
-    <link href="/assets/global/fonts/font-awesome/css/all.min.css" rel="stylesheet">
-    <link href="/assets/global/vendor/webui-popover/webui-popover.min.css" rel="stylesheet">
-    <link href="/assets/global/vendor/jvectormap/jquery-jvectormap.min.css" rel="stylesheet">
-    <style>
-        .flag-icon-rounded {
-            border-radius: 50%;
-            background-size: cover;
-            height: 100%;
-            width: auto;
-            aspect-ratio: 1 / 1;
-        }
-    </style>
-@endsection
-@section('content')
-    <div class="page-content container-fluid">
-        <div class="row">
-            <div class="col-md-9">
-                <div class="card card-inverse card-shadow bg-white map">
-                    <div class="card-block h-450">
-                        <div class="h-p100" id="world-map"></div>
-                    </div>
-                </div>
-            </div>
-            <div class="col-md-3">
-                <div class="row map">
-                    <div class="col-md-12">
-                        <div class="card card-block p-20 bg-indigo-500">
-                            <div class="counter counter-lg counter-inverse">
-                                <div class="counter-label text-uppercase font-size-16">{{ trans('user.account.level') }}</div>
-                                <div class="counter-number-group">
-                                    <span class="counter-icon"><i class="icon wb-user-circle" aria-hidden="true"></i></span>
-                                    <span class="counter-number ml-10">{{ auth()->user()->level }}</span>
-                                </div>
-                                <div class="counter-label text-uppercase font-size-16">{{ auth()->user()->level_name }}</div>
-                            </div>
-                        </div>
-                    </div>
-                    @if (auth()->user()->user_group_id)
-                        <div class="col-md-12">
-                            <div class="card card-block p-30 bg-indigo-500">
-                                <div class="counter counter-lg counter-inverse">
-                                    <div class="counter-label text-uppercase font-size-16">{{ trans('user.account.group') }}</div>
-                                    <div class="counter-number-group">
-                                        <span class="counter-icon"><i class="icon wb-globe" aria-hidden="true"></i></span>
-                                        <span class="counter-number ml-10">{{ auth()->user()->userGroup->name }}</span>
-                                    </div>
-                                </div>
-                            </div>
-                        </div>
-                    @endif
-                    <div class="col-md-12">
-                        <div class="card card-block p-30 bg-indigo-500">
-                            <div class="counter counter-lg counter-inverse">
-                                <div class="counter-label text-uppercase font-size-16">{{ trans('user.account.speed_limit') }}</div>
-                                <div class="counter-number-group">
-                                    <span class="counter-icon"><i class="icon wb-signal" aria-hidden="true"></i></span>
-                                    <span class="counter-number ml-10">{{ auth()->user()->speed_limit ?: trans('common.unlimited') }}</span>
-                                </div>
-                                <div class="counter-label font-size-16">Mbps</div>
-                            </div>
-                        </div>
-                    </div>
-                </div>
-            </div>
-            @foreach ($nodes as $node)
-                <div class="col-xxl-3 col-xl-4 col-sm-6">
-                    <div class="card card-inverse card-shadow bg-white node-card">
-                        <div class="card-block p-30 row">
-                            <div class="col-3">
-                                <i class="fi fi-{{ $node->country_code }} flag-icon-rounded" aria-hidden="true"></i>
-                            </div>
-                            <div class="col-9 text-break text-right">
-                                <p class="font-size-20 blue-600">
-                                    <span class="float-left badge badge-round badge-default">{{ $node->level_table->name }}</span>
-                                    @if ($node->offline && !$node->relay_node_id)
-                                        <i class="red-600 icon wb-warning" data-content="{{ trans('user.node.unstable') }}" data-trigger="hover"
-                                           data-toggle="popover" data-placement="top"></i>
-                                    @endif
-                                    @if ($node->traffic_rate !== 1.0)
-                                        <i class="green-600 icon wb-info-circle" data-content="{{ trans('user.node.rate', ['ratio' => $node->traffic_rate]) }}"
-                                           data-trigger="hover" data-toggle="popover" data-placement="top"></i>
-                                    @endif
-                                    {{ $node->name }}
-                                </p>
-                                <blockquote>
-                                    @foreach ($node->label_names->take(3) as $label_name)
-                                        <span class="badge badge-lg badge-round badge-info">{{ $label_name }}</span>
-                                    @endforeach
-                                    @if ($node->label_names->count() > 3)
-                                        <i class="icon wb-more-horizontal" data-content="{{ $node->label_names->join(', ') }}" data-trigger="hover"
-                                           data-toggle="popover" data-placement="top"></i>
-                                    @endif
-                                    <br>
-                                    {{ $node->description }}
-                                </blockquote>
-                                <div>
-                                    <button class="btn btn-sm btn-outline-info" onclick="getInfo('{{ $node->id }}','code')">
-                                        <i class="fa-solid fa-code" id="code{{ $node->id }}"></i>
-                                    </button>
-                                    <button class="btn btn-sm btn-outline-info" onclick="getInfo('{{ $node->id }}','qrcode')">
-                                        <i class="fa-solid fa-qrcode" id="qrcode{{ $node->id }}"></i>
-                                    </button>
-                                    <button class="btn btn-sm btn-outline-info" onclick="getInfo('{{ $node->id }}','text')">
-                                        <i class="fa-solid fa-list" id="text{{ $node->id }}"></i>
-                                    </button>
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                </div>
-            @endforeach
-        </div>
-    </div>
-@endsection
-@section('javascript')
-    <script src="/assets/global/vendor/matchheight/jquery.matchHeight-min.js" type="text/javascript"></script>
-    <script src="/assets/global/js/Plugin/matchheight.js" type="text/javascript"></script>
-    <script src="/assets/custom/easy.qrcode.min.js" type="text/javascript"></script>
-    <script src="/assets/global/js/Plugin/webui-popover.js" type="text/javascript"></script>
-    <script src="/assets/global/vendor/jvectormap/jquery-jvectormap.min.js"></script>
-    <script src="/assets/custom/maps/jquery-jvectormap-world-mill-cn.js"></script>
-
-    <script type="text/javascript">
-        $(function() {
-            $("#world-map").vectorMap({
-                map: "world_mill",
-                scaleColors: ["#C8EEFF", "#0071A4"],
-                normalizeFunction: "polynomial",
-                zoomAnimate: true,
-                hoverOpacity: 0.7,
-                hoverColor: false,
-                regionStyle: {
-                    initial: {
-                        fill: "#3E8EF7"
-                    },
-                    hover: {
-                        fill: "#589FFC"
-                    },
-                    selected: {
-                        fill: "#0B69E3"
-                    },
-                    selectedHover: {
-                        fill: "#589FFC"
-                    }
-                },
-                markerStyle: {
-                    initial: {
-                        r: 3,
-                        fill: "#FF4C52",
-                        "stroke-width": 0
-                    },
-                    hover: {
-                        r: 6,
-                        stroke: "#FF4C52",
-                        "stroke-width": 0
-                    }
-                },
-                backgroundColor: "#fff",
-                markers: [
-                    @foreach ($nodesGeo as $geo => $name)
-                        {
-                            latLng: [{{ $geo }}],
-                            name: '{{ $name }}'
-                        },
-                    @endforeach
-                ]
-            });
-            $(".node-card").matchHeight();
-            $(".map").matchHeight();
-        });
-
-        function getInfo(id, type) {
-            const oldClass = $(`#${type}${id}`).attr("class");
-            const iconElement = $(`#${type}${id}`);
-
-            ajaxPost(jsRoute('{{ route('node.show', 'PLACEHOLDER') }}', id), {
-                type: type
-            }, {
-                beforeSend: function() {
-                    iconElement.removeClass().addClass("icon wb-loop icon-spin");
-                },
-                success: function(ret) {
-                    if (ret.status === "success") {
-                        switch (type) {
-                            case "code":
-                                swal.fire({
-                                    html: "<textarea class=\"form-control\" rows=\"8\" readonly=\"readonly\">" + ret.data + "</textarea>" +
-                                        "<a href=\"" + ret.data + '" class="btn btn-block btn-danger mt-4">{{ trans('common.open') }}' +
-                                        ret.title + "</a>",
-                                    showConfirmButton: false
-                                });
-                                break;
-                            case "qrcode":
-                                swal.fire({
-                                    title: '{{ trans('user.scan_qrcode') }}',
-                                    html: '<div id="qrcode"></div><button class="btn btn-block btn-outline-primary mt-4" onclick="Download()"> <i class="icon wb-download"></i> {{ trans('common.download') }}</button>',
-                                    onBeforeOpen: () => {
-                                        new QRCode(document.getElementById("qrcode"), {
-                                            text: ret.data
-                                        });
-                                    },
-                                    showConfirmButton: false
-                                });
-                                break;
-                            case "text":
-                                swal.fire({
-                                    title: '{{ trans('user.node.info') }}',
-                                    html: "<textarea class=\"form-control\" rows=\"12\" readonly=\"readonly\">" + ret.data + "</textarea>",
-                                    showConfirmButton: false
-                                });
-                                break;
-                            default:
-                                swal.fire({
-                                    title: ret.title,
-                                    text: ret.data,
-                                    icon: "error"
-                                });
-                        }
-                    }
-                },
-                complete: function() {
-                    iconElement.removeClass().addClass(oldClass);
-                }
-            });
-        }
-
-        function Download() {
-            const canvas = document.getElementsByTagName("canvas")[0];
-            canvas.toBlob((blob) => {
-                let link = document.createElement("a");
-                link.download = "qr.png";
-
-                let reader = new FileReader();
-                reader.readAsDataURL(blob);
-                reader.onload = () => {
-                    link.href = reader.result;
-                    link.click();
-                };
-            }, "image/png");
-        }
-    </script>
-@endsection

+ 4 - 4
resources/views/user/profile.blade.php

@@ -54,9 +54,9 @@
                                     </span>
                                     <span>
                                         @if (in_array($provider, $auth, true))
-                                            <span class="text-danger">{{ trans('user.oauth.rebind') }}</span>
+                                            <span class="text-danger"><i class="fa-solid fa-arrows-rotate"></i> {{ trans('user.oauth.rebind') }}</span>
                                         @else
-                                            <span class="text-muted">{{ trans('user.oauth.not_bind') }}</span>
+                                            <span class="text-muted"><i class="fa-solid fa-link"></i> {{ trans('user.oauth.not_bind') }}</span>
                                         @endif
                                         @if ($provider === 'telegram')
                                             <script async src="https://telegram.org/js/telegram-widget.js?22" data-telegram-login="{{ config('services.telegram.bot') }}" data-size="medium"
@@ -65,8 +65,8 @@
                                     </span>
                                 </a>
                                 @if (in_array($provider, $auth, true))
-                                    <a class="col-2 btn btn-danger btn-block my-auto"
-                                       href="{{ route('oauth.unbind', ['provider' => $provider]) }}">{{ trans('user.oauth.unbind') }}</a>
+                                    <a class="col-2 btn btn-danger btn-block my-auto" href="{{ route('oauth.unbind', ['provider' => $provider]) }}"><i
+                                           class="fa-solid fa-link-slash"></i> {{ trans('user.oauth.unbind') }}</a>
                                 @endif
                             @endforeach
                         </div>

+ 206 - 217
resources/views/user/services.blade.php

@@ -1,255 +1,244 @@
 @extends('user.layouts')
 @section('css')
-    <link href="assets/global/vendor/ionrangeslider/ionrangeslider.min.css" rel="stylesheet">
+    <link href="/assets/global/fonts/font-awesome/css/all.min.css" rel="stylesheet">
+    <link href="/assets/global/vendor/webui-popover/webui-popover.min.css" rel="stylesheet">
+    <link href="/assets/global/vendor/jvectormap/jquery-jvectormap.min.css" rel="stylesheet">
+    <style>
+        .flag-icon-rounded {
+            border-radius: 50%;
+            background-size: cover;
+            height: 100%;
+            width: auto;
+            aspect-ratio: 1 / 1;
+        }
+    </style>
 @endsection
 @section('content')
-    <div class="page-content">
+    <div class="page-content container-fluid">
         <div class="row">
-            <div class="col-xxl-2 col-lg-3">
-                <div class="card card-shadow">
-                    <div class="card-block p-20">
-                        <button class="btn btn-floating btn-sm btn-pure" type="button">
-                            <i class="icon wb-payment green-500"></i>
-                        </button>
-                        <span class="font-weight-400">{{ trans('user.account.credit') }}</span>
-                        <div class="content-text text-center mb-0">
-                            <span class="font-size-40 font-weight-100">{{ auth()->user()->credit_tag }}</span>
-                            <br />
-                            <button class="btn btn-danger float-right mr-15" data-toggle="modal" data-target="#charge_modal">{{ trans('user.recharge') }}</button>
-                        </div>
+            <div class="col-md-9">
+                <div class="card card-inverse card-shadow bg-white map">
+                    <div class="card-block h-450">
+                        <div class="h-p100" id="world-map"></div>
                     </div>
                 </div>
-                @if ($renewTraffic)
-                    <div class="card card-shadow">
-                        <div class="card-block p-20">
-                            <button class="btn btn-floating btn-sm btn-pure" type="button">
-                                <i class="icon wb-payment green-500"></i>
-                            </button>
-                            <span class="font-weight-400">{{ trans('user.reset_data.action') }}</span>
-                            <div class="content-text text-center mb-0">
-                                <span class="font-size-20 font-weight-100">{!! trans('user.reset_data.cost', ['amount' => $renewTraffic]) !!}</span>
-                                <br />
-                                <button class="btn btn-danger mt-10" onclick="resetTraffic()">{{ trans('common.reset') }}</button>
+            </div>
+            <div class="col-md-3">
+                <div class="row map">
+                    <div class="col-md-12">
+                        <div class="card card-block p-20 bg-indigo-500">
+                            <div class="counter counter-lg counter-inverse">
+                                <div class="counter-label text-uppercase font-size-16">{{ trans('user.account.level') }}</div>
+                                <div class="counter-number-group">
+                                    <span class="counter-icon"><i class="icon wb-user-circle" aria-hidden="true"></i></span>
+                                    <span class="counter-number ml-10">{{ auth()->user()->level }}</span>
+                                </div>
+                                <div class="counter-label text-uppercase font-size-16">{{ auth()->user()->level_name }}</div>
                             </div>
                         </div>
                     </div>
-                @endif
-            </div>
-            <div class="col-xxl-10 col-lg-9">
-                <div class="panel">
-                    <div class="panel-heading p-20">
-                        <h1 class="panel-title cyan-700">
-                            <i class="icon wb-shopping-cart"></i>{{ trans('user.menu.shop') }}
-                        </h1>
-                    </div>
-                    <div class="panel-body">
-                        <div class="row">
-                            @foreach ($goodsList as $goods)
-                                <div class="col-md-6 col-xl-4 col-xxl-3">
-                                    <div class="position-relative">
-                                        @if ($goods->limit_num)
-                                            <div class="ribbon ribbon-badge ribbon-danger ribbon-reverse">
-                                                <span class="ribbon-inner">{{ trans('user.shop.limited') }}</span>
-                                            </div>
-                                        @elseif($goods->is_hot)
-                                            <div class="ribbon ribbon-badge ribbon-danger ribbon-reverse">
-                                                <span class="ribbon-inner">{{ trans('user.shop.hot') }}</span>
-                                            </div>
-                                        @endif
-                                    </div>
-                                    <div class="pricing-list text-left">
-                                        <div class="pricing-header text-white" style="background-color: {{ $goods->color }}">
-                                            <div class="pricing-title font-size-20">{{ $goods->name }}</div>
-                                            <div class="pricing-price text-white @if ($goods->type === 1) text-center @endif">
-                                                <span class="pricing-amount">{{ $goods->price_tag }}</span>
-                                                @if ($goods->type === 2)
-                                                    <span class="pricing-period">/ {{ $goods->days . trans_choice('common.days.attribute', 1) }}</span>
-                                                @endif
-                                            </div>
-                                            @if ($goods->description)
-                                                <p class="px-30 pb-25 text-center">{{ $goods->description }}</p>
-                                            @endif
-                                        </div>
-                                        <ul class="pricing-features">
-                                            <li>
-                                                <strong>{{ $goods->traffic_label }}</strong>{{ trans('user.attribute.data') }}
-                                                {!! $goods->type === 1 ? "<code> $dataPlusDays </code>" . trans_choice('common.days.attribute', 1) : '/' . ucfirst(trans('validation.attributes.month')) !!}
-                                            </li>
-                                            <li>
-                                                {!! trans('user.service.node_count', ['num' => $goods->node_count]) !!}
-                                            </li>
-                                            <li>
-                                                {{ trans('user.account.speed_limit') }}
-                                                <strong> {{ $goods->speed_limit ? $goods->speed_limit . ' Mbps' : trans('user.service.unlimited') }} </strong>
-                                            </li>
-                                            {!! $goods->info !!}
-                                        </ul>
-                                        <div class="pricing-footer text-center bg-blue-grey-100">
-                                            <a class="btn btn-lg btn-primary" href="{{ route('shop.show', $goods) }}"> {{ trans('user.shop.buy') }}</a>
-                                        </div>
+                    @if (auth()->user()->user_group_id)
+                        <div class="col-md-12">
+                            <div class="card card-block p-30 bg-indigo-500">
+                                <div class="counter counter-lg counter-inverse">
+                                    <div class="counter-label text-uppercase font-size-16">{{ trans('user.account.group') }}</div>
+                                    <div class="counter-number-group">
+                                        <span class="counter-icon"><i class="icon wb-globe" aria-hidden="true"></i></span>
+                                        <span class="counter-number ml-10">{{ auth()->user()->userGroup->name }}</span>
                                     </div>
                                 </div>
-                            @endforeach
+                            </div>
+                        </div>
+                    @endif
+                    <div class="col-md-12">
+                        <div class="card card-block p-30 bg-indigo-500">
+                            <div class="counter counter-lg counter-inverse">
+                                <div class="counter-label text-uppercase font-size-16">{{ trans('user.account.speed_limit') }}</div>
+                                <div class="counter-number-group">
+                                    <span class="counter-icon"><i class="icon wb-signal" aria-hidden="true"></i></span>
+                                    <span class="counter-number ml-10">{{ auth()->user()->speed_limit ?: trans('common.unlimited') }}</span>
+                                </div>
+                                <div class="counter-label font-size-16">Mbps</div>
+                            </div>
                         </div>
                     </div>
                 </div>
             </div>
-        </div>
-    </div>
-    <div class="modal fade" id="charge_modal" role="dialog" aria-labelledby="charge_modal" aria-hidden="true" tabindex="-1">
-        <div class="modal-dialog modal-simple modal-center">
-            <div class="modal-content">
-                <div class="modal-header">
-                    <button class="close" data-dismiss="modal" type="button" aria-label="{{ trans('common.close') }}">
-                        <span aria-hidden="true">×</span></button>
-                    <h4 class="modal-title">{{ trans('user.recharge_credit') }}</h4>
-                </div>
-                <div class="modal-body">
-                    <div class="alert alert-danger" id="charge_msg" style="display: none;"></div>
-                    <form action="#" method="post">
-                        @if (sysConfig('is_onlinePay') || sysConfig('alipay_qrcode') || sysConfig('wechat_qrcode'))
-                            <div class="mb-15 w-p50">
-                                <select class="form-control" id="charge_type" name="charge_type">
-                                    @if (sysConfig('is_onlinePay') || sysConfig('alipay_qrcode') || sysConfig('wechat_qrcode'))
-                                        <option value="1">{{ trans('user.shop.pay_online') }}</option>
-                                    @endif
-                                    <option value="2">{{ trans('admin.coupon.type.charge') }}</option>
-                                </select>
+            @foreach ($nodes as $node)
+                <div class="col-xxl-3 col-xl-4 col-sm-6">
+                    <div class="card card-inverse card-shadow bg-white node-card">
+                        <div class="card-block p-30 row">
+                            <div class="col-3">
+                                <i class="fi fi-{{ $node->country_code }} flag-icon-rounded" aria-hidden="true"></i>
                             </div>
-                        @endif
-                        @if (sysConfig('is_onlinePay') || sysConfig('alipay_qrcode') || sysConfig('wechat_qrcode'))
-                            <div class="form-group row charge_credit">
-                                <label class="offset-md-1 col-md-2 col-form-label" for="amount">{{ trans('user.shop.change_amount') }}</label>
-                                <div class="col-md-8">
-                                    <input id="amount" name="amount" data-plugin="ionRangeSlider" data-min=1 data-max=300 data-from=40
-                                           data-prefix="{{ array_column(config('common.currency'), 'symbol', 'code')[session('currency') ?? sysConfig('standard_currency')] }}"
-                                           type="text" />
+                            <div class="col-9 text-break text-right">
+                                <p class="font-size-20 blue-600">
+                                    <span class="float-left badge badge-round badge-default">{{ $node->level_table->name }}</span>
+                                    @if ($node->offline && !$node->relay_node_id)
+                                        <i class="red-600 icon wb-warning" data-content="{{ trans('user.node.unstable') }}" data-trigger="hover"
+                                           data-toggle="popover" data-placement="top"></i>
+                                    @endif
+                                    @if ($node->traffic_rate !== 1.0)
+                                        <i class="green-600 icon wb-info-circle" data-content="{{ trans('user.node.rate', ['ratio' => $node->traffic_rate]) }}"
+                                           data-trigger="hover" data-toggle="popover" data-placement="top"></i>
+                                    @endif
+                                    {{ $node->name }}
+                                </p>
+                                <blockquote>
+                                    @foreach ($node->label_names->take(3) as $label_name)
+                                        <span class="badge badge-lg badge-round badge-info">{{ $label_name }}</span>
+                                    @endforeach
+                                    @if ($node->label_names->count() > 3)
+                                        <i class="icon wb-more-horizontal" data-content="{{ $node->label_names->join(', ') }}" data-trigger="hover"
+                                           data-toggle="popover" data-placement="top"></i>
+                                    @endif
+                                    <br>
+                                    {{ $node->description }}
+                                </blockquote>
+                                <div>
+                                    <button class="btn btn-sm btn-outline-info" onclick="getInfo('{{ $node->id }}','code')">
+                                        <i class="fa-solid fa-code" id="code{{ $node->id }}"></i>
+                                    </button>
+                                    <button class="btn btn-sm btn-outline-info" onclick="getInfo('{{ $node->id }}','qrcode')">
+                                        <i class="fa-solid fa-qrcode" id="qrcode{{ $node->id }}"></i>
+                                    </button>
+                                    <button class="btn btn-sm btn-outline-info" onclick="getInfo('{{ $node->id }}','text')">
+                                        <i class="fa-solid fa-list" id="text{{ $node->id }}"></i>
+                                    </button>
                                 </div>
                             </div>
-                        @endif
-                        <div class="form-group row" id="charge_coupon_code">
-                            <label class="offset-md-2 col-md-2 col-form-label" for="charge_coupon"> {{ trans('admin.coupon.type.charge') }} </label>
-                            <div class="col-md-6">
-                                <input class="form-control round" id="charge_coupon" name="charge_coupon" type="text"
-                                       placeholder="{{ trans('user.coupon.input') }}">
-                            </div>
                         </div>
-                    </form>
-                </div>
-                <div class="modal-footer">
-                    <div class="charge_credit">
-                        @include('user.components.purchase')
                     </div>
-                    <button class="btn btn-primary" id="change_btn" type="button" onclick="pay()">{{ trans('user.recharge') }}</button>
                 </div>
-            </div>
+            @endforeach
         </div>
     </div>
 @endsection
 @section('javascript')
-    <script src="assets/global/vendor/ionrangeslider/ion.rangeSlider.min.js"></script>
-    <script src="assets/global/js/Plugin/ionrangeslider.js"></script>
-    <script>
-        function itemControl(value) {
-            const control = value === 1;
-            $('.charge_credit').toggle(control);
-            $('#change_btn').toggle(!control);
-            $('#charge_coupon_code').toggle(!control);
-        }
-
-        $(document).ready(function() {
-            let which_selected = 2;
-            @if (sysConfig('is_onlinePay') || sysConfig('alipay_qrcode') || sysConfig('wechat_qrcode'))
-                which_selected = 1;
-            @endif
+    <script src="/assets/global/vendor/matchheight/jquery.matchHeight-min.js" type="text/javascript"></script>
+    <script src="/assets/global/js/Plugin/matchheight.js" type="text/javascript"></script>
+    <script src="/assets/custom/easy.qrcode.min.js" type="text/javascript"></script>
+    <script src="/assets/global/js/Plugin/webui-popover.js" type="text/javascript"></script>
+    <script src="/assets/global/vendor/jvectormap/jquery-jvectormap.min.js"></script>
+    <script src="/assets/custom/maps/jquery-jvectormap-world-mill-cn.js"></script>
 
-            itemControl(which_selected);
-            $('#charge_type').val(which_selected);
+    <script type="text/javascript">
+        $(function() {
+            $("#world-map").vectorMap({
+                map: "world_mill",
+                scaleColors: ["#C8EEFF", "#0071A4"],
+                normalizeFunction: "polynomial",
+                zoomAnimate: true,
+                hoverOpacity: 0.7,
+                hoverColor: false,
+                regionStyle: {
+                    initial: {
+                        fill: "#3E8EF7"
+                    },
+                    hover: {
+                        fill: "#589FFC"
+                    },
+                    selected: {
+                        fill: "#0B69E3"
+                    },
+                    selectedHover: {
+                        fill: "#589FFC"
+                    }
+                },
+                markerStyle: {
+                    initial: {
+                        r: 3,
+                        fill: "#FF4C52",
+                        "stroke-width": 0
+                    },
+                    hover: {
+                        r: 6,
+                        stroke: "#FF4C52",
+                        "stroke-width": 0
+                    }
+                },
+                backgroundColor: "#fff",
+                markers: [
+                    @foreach ($nodesGeo as $geo => $name)
+                        {
+                            latLng: [{{ $geo }}],
+                            name: '{{ $name }}'
+                        },
+                    @endforeach
+                ]
+            });
+            $(".node-card").matchHeight();
+            $(".map").matchHeight();
         });
 
-        // 切换充值方式
-        $('#charge_type').change(function() {
-            itemControl(parseInt($(this).val()));
-        });
+        function getInfo(id, type) {
+            const oldClass = $(`#${type}${id}`).attr("class");
+            const iconElement = $(`#${type}${id}`);
 
-        // 重置流量
-        function resetTraffic() {
-            showConfirm({
-                title: '{{ trans('user.reset_data.action') }}',
-                text: '{{ trans('user.reset_data.cost_tips', ['amount' => $renewTraffic]) }}',
-                onConfirm: function() {
-                    ajaxPost('{{ route('shop.resetTraffic') }}');
+            ajaxPost(jsRoute('{{ route('node.show', 'PLACEHOLDER') }}', id), {
+                type: type
+            }, {
+                beforeSend: function() {
+                    iconElement.removeClass().addClass("icon wb-loop icon-spin");
+                },
+                success: function(ret) {
+                    if (ret.status === "success") {
+                        switch (type) {
+                            case "code":
+                                swal.fire({
+                                    html: "<textarea class=\"form-control\" rows=\"8\" readonly=\"readonly\">" + ret.data + "</textarea>" +
+                                        "<a href=\"" + ret.data + '" class="btn btn-block btn-danger mt-4">{{ trans('common.open') }}' +
+                                        ret.title + "</a>",
+                                    showConfirmButton: false
+                                });
+                                break;
+                            case "qrcode":
+                                swal.fire({
+                                    title: '{{ trans('user.scan_qrcode') }}',
+                                    html: '<div id="qrcode"></div><button class="btn btn-block btn-outline-primary mt-4" onclick="Download()"> <i class="icon wb-download"></i> {{ trans('common.download') }}</button>',
+                                    onBeforeOpen: () => {
+                                        new QRCode(document.getElementById("qrcode"), {
+                                            text: ret.data
+                                        });
+                                    },
+                                    showConfirmButton: false
+                                });
+                                break;
+                            case "text":
+                                swal.fire({
+                                    title: '{{ trans('user.node.info') }}',
+                                    html: "<textarea class=\"form-control\" rows=\"12\" readonly=\"readonly\">" + ret.data + "</textarea>",
+                                    showConfirmButton: false
+                                });
+                                break;
+                            default:
+                                swal.fire({
+                                    title: ret.title,
+                                    text: ret.data,
+                                    icon: "error"
+                                });
+                        }
+                    }
+                },
+                complete: function() {
+                    iconElement.removeClass().addClass(oldClass);
                 }
             });
         }
 
-        // 充值
-        function pay(method, pay_type) {
-            const paymentType = parseInt($('#charge_type').val() ?? 2);
-            const charge_coupon = $('#charge_coupon').val().trim();
-            const amount = parseInt($('#amount').val());
-            if (paymentType === 1) {
-                if (amount <= 0) {
-                    showMessage({
-                        title: '{{ trans('common.error') }}',
-                        text: '{{ trans('user.payment.error') }}',
-                        icon: 'warning',
-                        showConfirmButton: false,
-                    });
-                    return false;
-                }
+        function Download() {
+            const canvas = document.getElementsByTagName("canvas")[0];
+            canvas.toBlob((blob) => {
+                let link = document.createElement("a");
+                link.download = "qr.png";
 
-                ajaxPost('{{ route('purchase') }}', {
-                    amount: amount,
-                    method: method,
-                    pay_type: pay_type
-                }, {
-                    beforeSend: function() {
-                        $('#charge_msg').show().html('{{ trans('user.payment.creating') }}');
-                    },
-                    success: function(ret) {
-                        $('#charge_msg').show().html(ret.message);
-                        if (ret.status === 'fail') {
-                            return false;
-                        } else {
-                            if (ret.data) {
-                                window.location.href = jsRoute('{{ route('orderDetail', 'PLACEHOLDER') }}', ret.data);
-                            } else if (ret.url) {
-                                window.location.href = ret.url;
-                            }
-                        }
-                    },
-                    error: function() {
-                        $('#charge_msg').show().html("{{ trans('user.error_response') }}");
-                    },
-                });
-            } else if (paymentType === 2) {
-                if (charge_coupon === '') {
-                    $('#charge_msg').show().html("{{ trans('validation.required', ['attribute' => trans('model.coupon.attribute')]) }}");
-                    $('#charge_coupon').focus();
-                    return false;
-                }
-
-                ajaxPost('{{ route('shop.coupon.redeem') }}', {
-                    coupon_sn: charge_coupon
-                }, {
-                    beforeSend: function() {
-                        $('#charge_msg').show().html("{{ trans('user.recharging') }}");
-                    },
-                    success: function(ret) {
-                        if (ret.status === 'fail') {
-                            $('#charge_msg').show().html(ret.message);
-                            return false;
-                        }
-
-                        $('#charge_modal').modal('hide');
-                        window.location.reload();
-                    },
-                    error: function() {
-                        $('#charge_msg').show().html("{{ trans('user.error_response') }}");
-                    },
-                });
-            }
+                let reader = new FileReader();
+                reader.readAsDataURL(blob);
+                reader.onload = () => {
+                    link.href = reader.result;
+                    link.click();
+                };
+            }, "image/png");
         }
     </script>
 @endsection

+ 252 - 0
resources/views/user/shop.blade.php

@@ -0,0 +1,252 @@
+@extends('user.layouts')
+@section('css')
+    <link href="assets/global/vendor/ionrangeslider/ionrangeslider.min.css" rel="stylesheet">
+@endsection
+@section('content')
+    <div class="page-content">
+        <div class="row">
+            <div class="col-xxl-2 col-lg-3">
+                <div class="card card-shadow">
+                    <div class="card-block p-20">
+                        <h4 class="card-title"><i class="icon wb-briefcase green-500"></i> {{ trans('user.account.credit') }}</h4>
+                        <div class="content-text text-center mb-0">
+                            <span class="font-size-40 font-weight-100">{{ auth()->user()->credit_tag }}</span>
+                            <br />
+                            <button class="btn btn-danger float-right mr-15" data-toggle="modal" data-target="#charge_modal">{{ trans('user.recharge') }}</button>
+                        </div>
+                    </div>
+                </div>
+                @if ($renewTraffic)
+                    <div class="card card-shadow">
+                        <div class="card-block p-20">
+                            <h4 class="card-title"><i class="icon wb-payment grey-500"></i> {{ trans('user.reset_data.action') }}</h4>
+                            <div class="content-text text-center mb-0">
+                                <span class="font-size-20 font-weight-100">{!! trans('user.reset_data.cost', ['amount' => $renewTraffic]) !!}</span>
+                                <br />
+                                <button class="btn btn-danger mt-10" onclick="resetTraffic()">{{ trans('common.reset') }}</button>
+                            </div>
+                        </div>
+                    </div>
+                @endif
+            </div>
+            <div class="col-xxl-10 col-lg-9">
+                <div class="panel">
+                    <div class="panel-heading p-20">
+                        <h1 class="panel-title cyan-700">
+                            <i class="icon wb-shopping-cart"></i>{{ trans('user.menu.shop') }}
+                        </h1>
+                    </div>
+                    <div class="panel-body">
+                        <div class="row">
+                            @foreach ($goodsList as $goods)
+                                <div class="col-md-6 col-xl-4 col-xxl-3">
+                                    <div class="position-relative">
+                                        @if ($goods->limit_num)
+                                            <div class="ribbon ribbon-badge ribbon-danger ribbon-reverse">
+                                                <span class="ribbon-inner">{{ trans('user.shop.limited') }}</span>
+                                            </div>
+                                        @elseif($goods->is_hot)
+                                            <div class="ribbon ribbon-badge ribbon-danger ribbon-reverse">
+                                                <span class="ribbon-inner">{{ trans('user.shop.hot') }}</span>
+                                            </div>
+                                        @endif
+                                    </div>
+                                    <div class="pricing-list text-left">
+                                        <div class="pricing-header text-white" style="background-color: {{ $goods->color }}">
+                                            <div class="pricing-title font-size-20">{{ $goods->name }}</div>
+                                            <div class="pricing-price text-white @if ($goods->type === 1) text-center @endif">
+                                                <span class="pricing-amount">{{ $goods->price_tag }}</span>
+                                                @if ($goods->type === 2)
+                                                    <span class="pricing-period">/ {{ $goods->days . trans_choice('common.days.attribute', 1) }}</span>
+                                                @endif
+                                            </div>
+                                            @if ($goods->description)
+                                                <p class="px-30 pb-25 text-center">{{ $goods->description }}</p>
+                                            @endif
+                                        </div>
+                                        <ul class="pricing-features">
+                                            <li>
+                                                <strong>{{ $goods->traffic_label }}</strong>{{ trans('user.attribute.data') }}
+                                                {!! $goods->type === 1 ? "<code> $dataPlusDays </code>" . trans_choice('common.days.attribute', 1) : '/' . ucfirst(trans('validation.attributes.month')) !!}
+                                            </li>
+                                            <li>
+                                                {!! trans('user.service.node_count', ['num' => $goods->node_count]) !!}
+                                            </li>
+                                            <li>
+                                                {!! trans('user.service.country_count', ['num' => $goods->node_countries->count()]) !!}
+                                            </li>
+                                            <li>
+                                                {{ trans('user.account.speed_limit') }}
+                                                <strong> {{ $goods->speed_limit ? $goods->speed_limit . ' Mbps' : trans('user.service.unlimited') }} </strong>
+                                            </li>
+                                            {!! $goods->info !!}
+                                        </ul>
+                                        <div class="pricing-footer text-center bg-blue-grey-100">
+                                            <a class="btn btn-lg btn-primary" href="{{ route('shop.show', $goods) }}"> {{ trans('user.shop.buy') }}</a>
+                                        </div>
+                                    </div>
+                                </div>
+                            @endforeach
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="modal fade" id="charge_modal" role="dialog" aria-labelledby="charge_modal" aria-hidden="true" tabindex="-1">
+        <div class="modal-dialog modal-simple modal-center">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <button class="close" data-dismiss="modal" type="button" aria-label="{{ trans('common.close') }}">
+                        <span aria-hidden="true">×</span></button>
+                    <h4 class="modal-title">{{ trans('user.recharge_credit') }}</h4>
+                </div>
+                <div class="modal-body">
+                    <div class="alert alert-danger" id="charge_msg" style="display: none;"></div>
+                    <form action="#" method="post">
+                        @if (sysConfig('is_onlinePay') || sysConfig('alipay_qrcode') || sysConfig('wechat_qrcode'))
+                            <div class="mb-15 w-p50">
+                                <select class="form-control" id="charge_type" name="charge_type">
+                                    @if (sysConfig('is_onlinePay') || sysConfig('alipay_qrcode') || sysConfig('wechat_qrcode'))
+                                        <option value="1">{{ trans('user.shop.pay_online') }}</option>
+                                    @endif
+                                    <option value="2">{{ trans('admin.coupon.type.charge') }}</option>
+                                </select>
+                            </div>
+                        @endif
+                        @if (sysConfig('is_onlinePay') || sysConfig('alipay_qrcode') || sysConfig('wechat_qrcode'))
+                            <div class="form-group row charge_credit">
+                                <label class="offset-md-1 col-md-2 col-form-label" for="amount">{{ trans('user.shop.change_amount') }}</label>
+                                <div class="col-md-8">
+                                    <input id="amount" name="amount" data-plugin="ionRangeSlider" data-min=1 data-max=300 data-from=40
+                                           data-prefix="{{ array_column(config('common.currency'), 'symbol', 'code')[session('currency') ?? sysConfig('standard_currency')] }}"
+                                           type="text" />
+                                </div>
+                            </div>
+                        @endif
+                        <div class="form-group row" id="charge_coupon_code">
+                            <label class="offset-md-2 col-md-2 col-form-label" for="charge_coupon"> {{ trans('admin.coupon.type.charge') }} </label>
+                            <div class="col-md-6">
+                                <input class="form-control round" id="charge_coupon" name="charge_coupon" type="text"
+                                       placeholder="{{ trans('user.coupon.input') }}">
+                            </div>
+                        </div>
+                    </form>
+                </div>
+                <div class="modal-footer">
+                    <div class="charge_credit">
+                        @include('user.components.purchase')
+                    </div>
+                    <button class="btn btn-primary" id="change_btn" type="button" onclick="pay()">{{ trans('user.recharge') }}</button>
+                </div>
+            </div>
+        </div>
+    </div>
+@endsection
+@section('javascript')
+    <script src="assets/global/vendor/ionrangeslider/ion.rangeSlider.min.js"></script>
+    <script src="assets/global/js/Plugin/ionrangeslider.js"></script>
+    <script>
+        function itemControl(value) {
+            const control = value === 1;
+            $('.charge_credit').toggle(control);
+            $('#change_btn').toggle(!control);
+            $('#charge_coupon_code').toggle(!control);
+        }
+
+        $(document).ready(function() {
+            let which_selected = 2;
+            @if (sysConfig('is_onlinePay') || sysConfig('alipay_qrcode') || sysConfig('wechat_qrcode'))
+                which_selected = 1;
+            @endif
+
+            itemControl(which_selected);
+            $('#charge_type').val(which_selected);
+        });
+
+        // 切换充值方式
+        $('#charge_type').change(function() {
+            itemControl(parseInt($(this).val()));
+        });
+
+        // 重置流量
+        function resetTraffic() {
+            showConfirm({
+                title: '{{ trans('user.reset_data.action') }}',
+                text: '{{ trans('user.reset_data.cost_tips', ['amount' => $renewTraffic]) }}',
+                onConfirm: function() {
+                    ajaxPost('{{ route('shop.resetTraffic') }}');
+                }
+            });
+        }
+
+        // 充值
+        function pay(method, pay_type) {
+            const paymentType = parseInt($('#charge_type').val() ?? 2);
+            const charge_coupon = $('#charge_coupon').val().trim();
+            const amount = parseInt($('#amount').val());
+            if (paymentType === 1) {
+                if (amount <= 0) {
+                    showMessage({
+                        title: '{{ trans('common.error') }}',
+                        text: '{{ trans('user.payment.error') }}',
+                        icon: 'warning',
+                        showConfirmButton: false,
+                    });
+                    return false;
+                }
+
+                ajaxPost('{{ route('purchase') }}', {
+                    amount: amount,
+                    method: method,
+                    pay_type: pay_type
+                }, {
+                    beforeSend: function() {
+                        $('#charge_msg').show().html('{{ trans('user.payment.creating') }}');
+                    },
+                    success: function(ret) {
+                        $('#charge_msg').show().html(ret.message);
+                        if (ret.status === 'fail') {
+                            return false;
+                        } else {
+                            if (ret.data) {
+                                window.location.href = jsRoute('{{ route('orderDetail', 'PLACEHOLDER') }}', ret.data);
+                            } else if (ret.url) {
+                                window.location.href = ret.url;
+                            }
+                        }
+                    },
+                    error: function() {
+                        $('#charge_msg').show().html("{{ trans('user.error_response') }}");
+                    },
+                });
+            } else if (paymentType === 2) {
+                if (charge_coupon === '') {
+                    $('#charge_msg').show().html("{{ trans('validation.required', ['attribute' => trans('model.coupon.attribute')]) }}");
+                    $('#charge_coupon').focus();
+                    return false;
+                }
+
+                ajaxPost('{{ route('shop.coupon.redeem') }}', {
+                    coupon_sn: charge_coupon
+                }, {
+                    beforeSend: function() {
+                        $('#charge_msg').show().html("{{ trans('user.recharging') }}");
+                    },
+                    success: function(ret) {
+                        if (ret.status === 'fail') {
+                            $('#charge_msg').show().html(ret.message);
+                            return false;
+                        }
+
+                        $('#charge_modal').modal('hide');
+                        window.location.reload();
+                    },
+                    error: function() {
+                        $('#charge_msg').show().html("{{ trans('user.error_response') }}");
+                    },
+                });
+            }
+        }
+    </script>
+@endsection

+ 1 - 3
resources/views/vendor/log-viewer/remark/layouts.blade.php

@@ -190,9 +190,7 @@
     @yield('modals')
 @endsection
 @section('layout_javascript')
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.6.0/chart.min.js"
-            integrity="sha512-GMGzUEevhWh8Tc/njS0bDpwgxdCJLQBWG3Z2Ct+JGOpVnEmjvNx6ts4v6A2XJf1HOrtOsfhv3hBKpK9kE5z8AQ==" crossorigin="anonymous"
-            referrerpolicy="no-referrer"></script>
+    <script src="/assets/global/vendor/chart-js/chart.umd.min.js"></script>
     <script>
         const $buoop = {
             required: {

Некоторые файлы не были показаны из-за большого количества измененных файлов