Files
saasshop/resources/views/admin/plans/index.blade.php

306 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@extends('admin.layouts.app')
@section('title', '套餐管理')
@section('page_title', '套餐管理')
@section('content')
@php
// back 参数用于“返回上一页(保留上下文)”,但 back 本身不应再包含 back避免无限嵌套导致 URL 膨胀)
$selfWithoutBack = \App\Support\BackUrl::selfWithoutBack();
// 用于构建“从套餐页跳转到订阅/订单页后可返回套餐页”的链接
$makeSubscriptionUrl = function (array $query) use ($selfWithoutBack) {
$query = $query + ['back' => $selfWithoutBack];
return '/admin/site-subscriptions?' . \Illuminate\Support\Arr::query($query);
};
$makePlatformOrderUrl = function (array $query) use ($selfWithoutBack) {
$query = $query + ['back' => $selfWithoutBack];
return '/admin/platform-orders?' . \Illuminate\Support\Arr::query($query);
};
// back 安全护栏(全页通用):
// - 仅允许站内相对路径(/ 开头)
// - 拒绝引号/尖括号(由于本页大量 href 采用 `{!! !!}` 原样输出,必须严控注入风险)
// - 拒绝 nested back=(避免 URL 膨胀/绕过)
$incomingBack = (string) request()->query('back', '');
$safeBackForLinks = \App\Support\BackUrl::sanitizeForLinks($incomingBack);
// 用于摘要卡等入口:保留当前 query 并覆盖字段,同时安全透传 back。
$safeFullUrlWithQuery = function (array $overrides = []) use ($safeBackForLinks) {
return \App\Support\BackUrl::currentPathWithQuery($overrides, $safeBackForLinks);
};
$planIndexUrl = \App\Support\BackUrl::withBack('/admin/plans', $safeBackForLinks);
@endphp
<div class="page-header mb-20" data-page="admin.plans.index">
<div class="page-header-main">
<div>
<div class="page-header-title">套餐管理</div>
<div class="page-header-subtitle">这里是总台视角的套餐目录页,用于沉淀平台可售卖的标准能力包。当前阶段先完成套餐主数据可见、可筛与口径收拢,后续再接授权项、售价规则与上下架动作。</div>
</div>
<div class="page-header-actions">
@if($safeBackForLinks !== '')
<a href="{!! $safeBackForLinks !!}" class="btn btn-secondary btn-sm">返回上一页(保留上下文)</a>
@endif
</div>
</div>
</div>
<div class="card mb-20">
<h3>快捷筛选</h3>
<div class="muted mb-10">用于运营快速定位需要处理的套餐集合(口径基于筛选条件组合)。</div>
@php
// 快捷筛选仅保留“上下文”字段back/keyword避免把其它筛选条件叠加导致空结果
$buildQuickFilterUrl = function (array $overrides) use ($safeBackForLinks) {
return \App\Support\BackUrl::currentPathQuickFilter(['keyword'], $overrides, $safeBackForLinks);
};
// “全部”:清空筛选,但保留安全 back用于返回来源页
$allUrl = \App\Support\BackUrl::withBack('/admin/plans', $safeBackForLinks);
@endphp
<div>
<a href="{!! $allUrl !!}" class="muted">全部</a>
<span class="muted"></span>
<a href="{!! $buildQuickFilterUrl(['status' => 'active']) !!}" class="muted">启用中</a>
<span class="muted"></span>
<a href="{!! $buildQuickFilterUrl(['status' => 'inactive']) !!}" class="muted">停用</a>
<span class="muted"></span>
<a href="{!! $buildQuickFilterUrl(['published' => 'published']) !!}" class="muted">已发布</a>
<span class="muted"></span>
<a href="{!! $buildQuickFilterUrl(['published' => 'unpublished']) !!}" class="muted">未发布</a>
</div>
</div>
<div class="card mb-20">
<h3>工具</h3>
<div class="grid-2">
<form method="get" action="/admin/plans/export">
<input type="hidden" name="download" value="1">
<input type="hidden" name="status" value="{{ $filters['status'] ?? '' }}">
<input type="hidden" name="published" value="{{ $filters['published'] ?? '' }}">
<input type="hidden" name="billing_cycle" value="{{ $filters['billing_cycle'] ?? '' }}">
<input type="hidden" name="keyword" value="{{ $filters['keyword'] ?? '' }}">
<button class="btn btn-secondary btn-sm" type="submit">导出 CSV</button>
</form>
<form method="post" action="/admin/plans/seed-defaults" onsubmit="return confirm('仅在当前没有任何套餐时才会初始化。确认执行吗?');" class="actions gap-10" data-action="disable-on-submit">
@csrf
<button class="btn btn-sm" type="submit">一键初始化默认套餐(空库)</button>
<div class="muted muted-xs mt-6">安全护栏:当库中已存在套餐时会自动阻止,避免污染运营数据。</div>
</form>
</div>
</div>
<details class="card mb-20 collapsible filters-card" data-role="collapsible" data-storage-key="admin.plans.filters" open>
<summary class="collapsible-summary filters-summary">
<div class="flex-between items-center">
<h3 class="mb-0">筛选条件</h3>
<span class="muted muted-xs">点击收起/展开</span>
</div>
</summary>
<div class="collapsible-body filters-body">
<form method="get" action="/admin/plans" class="grid-3 filters-grid">
@if($safeBackForLinks !== '')
<input type="hidden" name="back" value="{!! $safeBackForLinks !!}">
@endif
<select name="status">
<option value="">全部状态</option>
@foreach(($filterOptions['statuses'] ?? []) as $value => $label)
<option value="{{ $value }}" @selected(($filters['status'] ?? '') === $value)>{{ $label }}</option>
@endforeach
</select>
<select name="published">
<option value="">全部发布状态</option>
<option value="published" @selected(($filters['published'] ?? '') === 'published')>已发布</option>
<option value="unpublished" @selected(($filters['published'] ?? '') === 'unpublished')>未发布</option>
</select>
<select name="billing_cycle">
<option value="">全部计费周期</option>
@foreach(($filterOptions['billingCycles'] ?? []) as $value => $label)
<option value="{{ $value }}" @selected(($filters['billing_cycle'] ?? '') === $value)>{{ $label }}</option>
@endforeach
</select>
<input name="keyword" placeholder="搜索套餐名称 / 编码 / 描述" value="{{ $filters['keyword'] ?? '' }}">
<div>
<button class="btn btn-sm" type="submit">应用筛选</button>
</div>
</form>
</div>
</details>
<div class="grid-4 mb-20">
<div class="card">
<h3>套餐总数</h3>
<div class="num-md">
<a class="link" href="{!! $planIndexUrl !!}">{{ $summaryStats['total_plans'] ?? 0 }}</a>
</div>
</div>
<div class="card">
<h3>启用中套餐</h3>
<div class="num-md">
<a class="link" href="{!! $safeFullUrlWithQuery(['status' => 'active', 'page' => null]) !!}">{{ $summaryStats['active_plans'] ?? 0 }}</a>
</div>
</div>
<div class="card">
<h3>月付套餐</h3>
<div class="num-md">
<a class="link" href="{!! $safeFullUrlWithQuery(['billing_cycle' => 'monthly', 'page' => null]) !!}">{{ $summaryStats['monthly_plans'] ?? 0 }}</a>
</div>
</div>
<div class="card">
<h3>年付套餐</h3>
<div class="num-md">
<a class="link" href="{!! $safeFullUrlWithQuery(['billing_cycle' => 'yearly', 'page' => null]) !!}">{{ $summaryStats['yearly_plans'] ?? 0 }}</a>
</div>
</div>
<div class="card">
<h3>已发布套餐</h3>
<div class="num-md">
<a class="link" href="{!! $safeFullUrlWithQuery(['published' => 'published', 'page' => null]) !!}">{{ $summaryStats['published_plans'] ?? 0 }}</a>
</div>
</div>
<div class="card">
<h3>未发布套餐</h3>
<div class="num-md">
<a class="link" href="{!! $safeFullUrlWithQuery(['published' => 'unpublished', 'page' => null]) !!}">{{ $summaryStats['unpublished_plans'] ?? 0 }}</a>
</div>
</div>
<div class="card">
<h3>关联订阅总量</h3>
<div class="num-md">
<a class="link" href="{!! $makeSubscriptionUrl([]) !!}">{{ $summaryStats['subscriptions_count'] ?? 0 }}</a>
</div>
</div>
<div class="card">
<h3>关联平台订单总量</h3>
<div class="num-md">
<a class="link" href="{!! $makePlatformOrderUrl([]) !!}">{{ $summaryStats['platform_orders_count'] ?? 0 }}</a>
</div>
</div>
</div>
<div class="card list-card">
<div class="list-card-header">
<div>
<h3 class="list-card-title">套餐列表</h3>
<p class="muted muted-xs list-card-subtitle">后续将从这里进入套餐详情、授权项与订阅联动。</p>
</div>
@php
$createPlanUrl = \App\Support\BackUrl::withBack('/admin/plans/create', $selfWithoutBack);
@endphp
<a href="{!! $createPlanUrl !!}" class="btn btn-sm">新建套餐</a>
</div>
<div class="list-card-body">
<table class="list-card-table">
<thead>
<tr>
<th>ID</th>
<th>套餐名称</th>
<th>编码</th>
<th>计费周期</th>
<th>售价</th>
<th>划线价</th>
<th>状态</th>
<th>排序</th>
<th>发布时间</th>
<th>关联订阅</th>
<th>关联平台订单</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@forelse($plans as $plan)
<tr>
<td>{{ $plan->id }}</td>
<td>
<strong>{{ $plan->name }}</strong>
<div class="muted muted-xs">{{ $plan->description ?: '暂无说明' }}</div>
</td>
<td>{{ $plan->code }}</td>
<td>{{ $billingCycleLabels[$plan->billing_cycle] ?? $plan->billing_cycle }}</td>
<td>¥{{ number_format((float) $plan->price, 2) }}</td>
<td>¥{{ number_format((float) $plan->list_price, 2) }}</td>
<td>{{ $statusLabels[$plan->status] ?? $plan->status }}</td>
<td>{{ $plan->sort }}</td>
<td>{{ optional($plan->published_at)->format('Y-m-d H:i:s') ?: '-' }}</td>
<td>
@php $subCount = (int) ($plan->subscriptions_count ?? 0); @endphp
@if($subCount > 0)
<a class="link" href="{!! $makeSubscriptionUrl(['plan_id' => $plan->id]) !!}">{{ $subCount }} </a>
@else
<span class="muted">0</span>
@endif
</td>
<td>
@php $orderCount = (int) ($plan->platform_orders_count ?? 0); @endphp
@if($orderCount > 0)
<a class="link" href="{!! $makePlatformOrderUrl(['plan_id' => $plan->id]) !!}">{{ $orderCount }} </a>
@else
<span class="muted">0</span>
@endif
</td>
<td>
@php
$editPlanUrl = \App\Support\BackUrl::withBack('/admin/plans/' . $plan->id . '/edit', $selfWithoutBack);
$createOrderUrl = \App\Support\BackUrl::withBack('/admin/platform-orders/create?' . \Illuminate\Support\Arr::query([
'plan_id' => $plan->id,
'order_type' => 'new_purchase',
]), $selfWithoutBack);
@endphp
@php
$renewalMissingSubscriptionUrl = $makePlatformOrderUrl([
'plan_id' => $plan->id,
'renewal_missing_subscription' => '1',
]);
@endphp
<div class="actions gap-10">
<a href="{!! $editPlanUrl !!}" class="btn btn-secondary btn-sm">编辑</a>
@if((string) ($plan->status ?? '') === 'active')
<a href="{!! $createOrderUrl !!}" class="btn btn-sm">创建订单</a>
@else
<span class="muted muted-xs">未启用:不建议下单</span>
@endif
<a href="{!! $renewalMissingSubscriptionUrl !!}" class="btn btn-secondary btn-sm">续费缺订阅</a>
</div>
<form method="post" action="/admin/plans/{{ $plan->id }}/set-status" class="mt-6 actions gap-10">
@csrf
<select name="status" onchange="this.form.submit()" class="w-140">
@foreach(($filterOptions['statuses'] ?? []) as $value => $label)
<option value="{{ $value }}" @selected($plan->status === $value)>{{ $label }}</option>
@endforeach
</select>
<noscript><button type="submit" class="btn btn-secondary btn-sm">更新状态</button></noscript>
</form>
<form method="post" action="/admin/plans/{{ $plan->id }}/set-published" class="mt-6 actions gap-10">
@csrf
@php $isPublished = (bool) $plan->published_at; @endphp
<select name="published" onchange="this.form.submit()" class="w-140">
<option value="1" @selected($isPublished)>已发布</option>
<option value="0" @selected(!$isPublished)>未发布</option>
</select>
<noscript><button type="submit" class="btn btn-secondary btn-sm">更新发布状态</button></noscript>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="12" class="muted table-empty">暂无套餐数据,当前阶段先把套餐主表与总台目录立起来,后续可继续接套餐创建、授权项与订阅关联。</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
<div class="pagination-wrap">{{ $plans->links('pagination.admin') }}</div>
@endsection