前端极简黑白重设计,并纳入客户端注册、站点设置与后台相关改动
- storefront 全站改为极简黑白配色:直角、细线分隔、卡片 hover 浅底、深色模式(跟随系统+手动切换) - 抽出 storefront/partials/card 复用卡片 - 一并提交此前暂存的客户端注册 API、站点设置、后台页面与测试 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
@extends('admin.layout', [
|
||||
'title' => 'Tstore Admin · 账户安全',
|
||||
'pageTitle' => '账户安全',
|
||||
'pageSubtitle' => '修改当前管理员的用户名、邮箱和登录密码。',
|
||||
'pageBadge' => 'Account',
|
||||
])
|
||||
|
||||
@section('content')
|
||||
<div class="split">
|
||||
<section class="panel soft">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>账户资料</h2>
|
||||
<p>这里可以直接修改当前登录管理员的名称和邮箱地址。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ route('webadmin.account.profile') }}" class="grid">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="field">
|
||||
<label>用户名</label>
|
||||
<input class="input" name="name" value="{{ old('name', $user->name) }}" placeholder="输入管理员名称" required>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>邮箱</label>
|
||||
<input class="input" type="email" name="email" value="{{ old('email', $user->email) }}" placeholder="输入管理员邮箱" required>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn" type="submit">更新资料</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="kv-grid">
|
||||
<div class="kv-card">
|
||||
<span class="kv-label">当前邮箱</span>
|
||||
<span class="kv-value">{{ $user->email }}</span>
|
||||
</div>
|
||||
<div class="kv-card">
|
||||
<span class="kv-label">当前用户名</span>
|
||||
<span class="kv-value">{{ $user->name ?: '未设置' }}</span>
|
||||
</div>
|
||||
<div class="kv-card">
|
||||
<span class="kv-label">创建时间</span>
|
||||
<span class="kv-value">{{ optional($user->created_at)->format('Y-m-d H:i') ?: '-' }}</span>
|
||||
</div>
|
||||
<div class="kv-card">
|
||||
<span class="kv-label">最近更新</span>
|
||||
<span class="kv-value">{{ optional($user->updated_at)->format('Y-m-d H:i') ?: '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>修改密码</h2>
|
||||
<p>输入当前密码后设置新密码。为了避免误操作,新密码必须至少 8 位,并重复确认一次。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ route('webadmin.account.password') }}" class="grid">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="field">
|
||||
<label>当前密码</label>
|
||||
<input class="input" type="password" name="current_password" placeholder="输入当前密码" required>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>新密码</label>
|
||||
<input class="input" type="password" name="new_password" placeholder="至少 8 位" required>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>确认新密码</label>
|
||||
<input class="input" type="password" name="new_password_confirmation" placeholder="再次输入新密码" required>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn" type="submit">更新密码</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -1,138 +1,191 @@
|
||||
@extends('admin.layout', [
|
||||
'title' => 'Tstore Admin · 分类管理',
|
||||
'pageTitle' => ($filters['type'] === 'plugin' ? '插件分类' : ($filters['type'] === 'theme' ? '主题分类' : '分类管理')),
|
||||
'pageSubtitle' => ($filters['type'] === 'plugin'
|
||||
? '维护插件分类,为插件 package 提供归类。'
|
||||
: ($filters['type'] === 'theme'
|
||||
? '维护主题分类,为主题 package 提供归类。'
|
||||
: '统一维护插件与主题分类,为 package 提供归类。')),
|
||||
])
|
||||
|
||||
@php
|
||||
$currentType = $filters['type'] ?? '';
|
||||
$entityLabel = $currentType === 'plugin' ? '插件分类' : ($currentType === 'theme' ? '主题分类' : '分类');
|
||||
$listHint = $currentType === 'plugin' ? '按类型和关键词筛选插件分类。'
|
||||
: ($currentType === 'theme' ? '按类型和关键词筛选主题分类。' : '按类型和关键词筛选分类列表。');
|
||||
$createHint = $currentType === 'plugin' ? '新建一个插件分类,用于给插件 package 归类。'
|
||||
: ($currentType === 'theme' ? '新建一个主题分类,用于给主题 package 归类。' : '新建一个分类,用于给 package 归类。');
|
||||
$typeLabels = [
|
||||
'plugin' => '插件',
|
||||
'theme' => '主题',
|
||||
];
|
||||
@endphp
|
||||
|
||||
@extends('admin.layout', [
|
||||
'title' => 'Tstore Admin · 分类管理',
|
||||
'pageTitle' => $entityLabel,
|
||||
'pageSubtitle' => '维护扩展分类、排序与描述,供前台展示和后台筛选共用。',
|
||||
'pageBadge' => 'Categories',
|
||||
])
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.category-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:16px}
|
||||
.category-card{padding:18px;border-radius:24px;border:1px solid var(--line);background:linear-gradient(180deg,#fff 0,#f8fbfc 100%);box-shadow:var(--shadow-md)}
|
||||
.category-card-head{display:flex;justify-content:space-between;gap:14px;align-items:flex-start;margin-bottom:14px}
|
||||
.category-card-head h3{margin:0;font-size:18px;line-height:1.18}
|
||||
.category-card-meta{margin-top:6px;color:var(--ink-soft);font-size:13px}
|
||||
.category-card-form{display:grid;gap:12px}
|
||||
.category-card-form .form-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
|
||||
.category-card-form textarea{min-height:86px}
|
||||
.category-card-actions{display:flex;justify-content:space-between;gap:10px;flex-wrap:wrap;align-items:center}
|
||||
.category-card-actions .form-actions{margin-top:0}
|
||||
@media (max-width:860px){.category-grid,.category-card-form .form-grid{grid-template-columns:1fr}}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="grid">
|
||||
<div class="panel">
|
||||
<section class="stats">
|
||||
<div class="stat"><span class="label">当前结果</span><span class="value">{{ $categories->total() }}</span><span class="hint">符合筛选条件的分类</span></div>
|
||||
<div class="stat"><span class="label">当前类型</span><span class="value">{{ $currentType === '' ? '全部' : strtoupper($currentType) }}</span><span class="hint">正在查看的分类类型</span></div>
|
||||
<div class="stat"><span class="label">页码</span><span class="value">{{ $categories->currentPage() }}/{{ max($categories->lastPage(), 1) }}</span><span class="hint">分页浏览</span></div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>筛选与查询</h2>
|
||||
<p>{{ $listHint }}</p>
|
||||
<p>按类型和关键词快速收拢分类范围,避免后台列表过长。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="get" class="toolbar">
|
||||
<div class="filters">
|
||||
<div class="field">
|
||||
<label>类型</label>
|
||||
<select name="type" class="select">
|
||||
<option value="">全部</option>
|
||||
<option value="plugin" @selected($filters['type']==='plugin')>插件</option>
|
||||
<option value="theme" @selected($filters['type']==='theme')>主题</option>
|
||||
<option value="plugin" @selected($filters['type'] === 'plugin')>插件</option>
|
||||
<option value="theme" @selected($filters['type'] === 'theme')>主题</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field" style="min-width:280px">
|
||||
<div class="field" style="flex:1 1 280px">
|
||||
<label>关键词</label>
|
||||
<input class="input" type="text" name="keyword" value="{{ $filters['keyword'] }}" placeholder="搜索 name / slug / description">
|
||||
<input class="input" type="text" name="keyword" value="{{ $filters['keyword'] }}" placeholder="搜索名称、slug 或描述">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="btn" type="submit">筛选</button>
|
||||
<button class="btn" type="submit">应用筛选</button>
|
||||
<a class="btn secondary" href="{{ route('webadmin.categories') }}">重置</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="split">
|
||||
<div class="panel">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>{{ $entityLabel }}列表</h2>
|
||||
<p>支持就地编辑名称、slug、类型、排序与描述。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list">
|
||||
@forelse ($categories as $category)
|
||||
<div class="row-card">
|
||||
<form method="post" action="{{ route('webadmin.categories.update', $category->id) }}" class="grid">
|
||||
@csrf @method('PUT')
|
||||
<div class="form-grid">
|
||||
<div class="field"><label>名称</label><input class="input" name="name" value="{{ $category->name }}" required></div>
|
||||
<div class="field"><label>Slug</label><input class="input" name="slug" value="{{ $category->slug }}" required></div>
|
||||
<div class="field"><label>类型</label>
|
||||
<select class="select" name="type">
|
||||
<option value="plugin" @selected($category->type==='plugin')>插件</option>
|
||||
<option value="theme" @selected($category->type==='theme')>主题</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field"><label>排序</label><input class="input" type="number" name="sort_order" value="{{ $category->sort_order }}"></div>
|
||||
</div>
|
||||
<div class="field"><label>描述</label><textarea name="description">{{ $category->description }}</textarea></div>
|
||||
<div class="tags">
|
||||
<span class="chip">关联扩展 {{ $category->packages_count }}</span>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="btn small" type="submit">保存分类</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="form-actions">
|
||||
<form method="post" action="{{ route('webadmin.categories.destroy', $category->id) }}" onsubmit="return confirm('确认删除这个分类吗?');">
|
||||
@csrf @method('DELETE')
|
||||
<button class="btn danger small" type="submit">删除分类</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="empty">当前还没有分类。</div>
|
||||
@endforelse
|
||||
</div>
|
||||
<div style="margin-top:14px">{{ $categories->links() }}</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="stack">
|
||||
<section class="panel soft">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>新建{{ $entityLabel }}</h2>
|
||||
<p>{{ $createHint }}</p>
|
||||
<p>先把常用分类建好,下面的小卡片再负责维护已有分类。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ route('webadmin.categories.store') }}" class="grid">
|
||||
@csrf
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="field">
|
||||
<label>类型</label>
|
||||
<select class="select" name="type" required>
|
||||
<option value="plugin" @selected($currentType === 'plugin')>插件</option>
|
||||
<option value="theme" @selected($currentType === 'theme')>主题</option>
|
||||
<option value="plugin" @selected(old('type', $currentType ?: 'plugin') === 'plugin')>插件</option>
|
||||
<option value="theme" @selected(old('type', $currentType) === 'theme')>主题</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>排序</label>
|
||||
<input class="input" name="sort_order" type="number" value="0">
|
||||
<input class="input" name="sort_order" type="number" value="{{ old('sort_order', 0) }}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>名称</label>
|
||||
<input class="input" name="name" placeholder="SEO" required>
|
||||
<input class="input" name="name" value="{{ old('name') }}" placeholder="SEO" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Slug</label>
|
||||
<input class="input" name="slug" placeholder="seo" required>
|
||||
<input class="input" name="slug" value="{{ old('slug') }}" placeholder="seo" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>描述</label>
|
||||
<textarea name="description" placeholder="补充说明这个分类的用途"></textarea>
|
||||
<textarea name="description" placeholder="补充这个分类主要归类哪些扩展">{{ old('description') }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn" type="submit">创建{{ $entityLabel }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>{{ $entityLabel }}列表</h2>
|
||||
<p>改成更紧凑的小卡片,方便快速浏览并就地修改。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="category-grid">
|
||||
@forelse ($categories as $category)
|
||||
<article class="category-card">
|
||||
<div class="category-card-head">
|
||||
<div>
|
||||
<h3>{{ $category->name }}</h3>
|
||||
<div class="category-card-meta">{{ $category->slug }}</div>
|
||||
</div>
|
||||
<div class="tags" style="margin-top:0">
|
||||
<span class="chip">{{ $typeLabels[$category->type] ?? $category->type }}</span>
|
||||
<span class="chip muted">关联扩展 {{ $category->packages_count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ route('webadmin.categories.update', $category->id) }}" class="category-card-form" id="category-update-{{ $category->id }}">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="field">
|
||||
<label>名称</label>
|
||||
<input class="input" name="name" value="{{ $category->name }}" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Slug</label>
|
||||
<input class="input" name="slug" value="{{ $category->slug }}" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>类型</label>
|
||||
<select class="select" name="type">
|
||||
<option value="plugin" @selected($category->type === 'plugin')>插件</option>
|
||||
<option value="theme" @selected($category->type === 'theme')>主题</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>排序</label>
|
||||
<input class="input" type="number" name="sort_order" value="{{ $category->sort_order }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>描述</label>
|
||||
<textarea name="description">{{ $category->description }}</textarea>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<div class="category-card-actions">
|
||||
<div class="form-actions">
|
||||
<button class="btn small" type="submit" form="category-update-{{ $category->id }}">保存分类</button>
|
||||
</div>
|
||||
<form method="post" action="{{ route('webadmin.categories.destroy', $category->id) }}" onsubmit="return confirm('确认删除这个分类吗?');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button class="btn danger small" type="submit">删除分类</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
@empty
|
||||
<div class="empty">当前还没有分类。</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
{{ $categories->links() }}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
@php
|
||||
$statusLabel = $filters['status'] === 'online' ? '在线' : ($filters['status'] === 'offline' ? '离线' : '全部');
|
||||
@endphp
|
||||
|
||||
@extends('admin.layout', [
|
||||
'title' => 'Tstore Admin · 站点接入',
|
||||
'pageTitle' => '站点接入',
|
||||
'pageSubtitle' => '查看哪些 Typecho 站点已注册、是否在线,以及最近一次上报状态。',
|
||||
'pageBadge' => 'Clients',
|
||||
])
|
||||
|
||||
@section('content')
|
||||
<div class="grid">
|
||||
<section class="stats">
|
||||
<div class="stat"><span class="label">已注册站点</span><span class="value">{{ $stats['registered_sites'] }}</span><span class="hint">完成 token 注册</span></div>
|
||||
<div class="stat"><span class="label">在线站点</span><span class="value">{{ $stats['online_sites'] }}</span><span class="hint">{{ $onlineWindowMinutes }} 分钟内有心跳</span></div>
|
||||
<div class="stat"><span class="label">离线站点</span><span class="value">{{ max($stats['registered_sites'] - $stats['online_sites'], 0) }}</span><span class="hint">超出在线窗口</span></div>
|
||||
<div class="stat"><span class="label">追踪用户数</span><span class="value">{{ $stats['tracked_users'] }}</span><span class="hint">所有站点上报总和</span></div>
|
||||
<div class="stat"><span class="label">当前筛选</span><span class="value">{{ $statusLabel }}</span><span class="hint">站点状态过滤</span></div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="toolbar">
|
||||
<div class="section-title" style="margin-bottom:0">
|
||||
<div>
|
||||
<h2>站点列表</h2>
|
||||
<p>插件端注册成功后会出现在这里,之后每次状态同步都会刷新最近心跳和用户数。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="get" class="filters">
|
||||
<div class="field">
|
||||
<label>状态</label>
|
||||
<select class="select" name="status">
|
||||
<option value="">全部</option>
|
||||
<option value="online" @selected($filters['status'] === 'online')>在线</option>
|
||||
<option value="offline" @selected($filters['status'] === 'offline')>离线</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field" style="flex:1 1 260px">
|
||||
<label>搜索</label>
|
||||
<input class="input" type="text" name="keyword" value="{{ $filters['keyword'] }}" placeholder="站点名、URL、Typecho 或 PHP 版本">
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="btn" type="submit">应用筛选</button>
|
||||
<a class="btn secondary" href="{{ route('webadmin.clients') }}">重置</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>站点</th>
|
||||
<th>状态</th>
|
||||
<th>用户数</th>
|
||||
<th>环境</th>
|
||||
<th>注册时间</th>
|
||||
<th>最近同步</th>
|
||||
<th>Token 尾缀</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($clients as $client)
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ $client->site_name ?: '未命名站点' }}</strong>
|
||||
<div class="muted">{{ $client->site_url }}</div>
|
||||
</td>
|
||||
<td><span class="chip {{ $client->is_online ? 'ok' : 'muted' }}">{{ $client->is_online ? '在线' : '离线' }}</span></td>
|
||||
<td>{{ $client->user_count }}</td>
|
||||
<td>
|
||||
<div>Typecho {{ $client->typecho_version ?: '-' }}</div>
|
||||
<div class="muted">PHP {{ $client->php_version ?: '-' }} · 插件 {{ $client->plugin_version ?: '-' }}</div>
|
||||
</td>
|
||||
<td>{{ optional($client->registered_at)->format('Y-m-d H:i:s') ?: '-' }}</td>
|
||||
<td>{{ optional($client->last_seen_at)->format('Y-m-d H:i:s') ?: '尚未同步' }}</td>
|
||||
<td><code>...{{ $client->token_suffix ?: '--------' }}</code></td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<div class="empty">当前还没有站点完成注册。</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{ $clients->links() }}
|
||||
</section>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -1,71 +1,146 @@
|
||||
@php
|
||||
$statusLabels = [
|
||||
'draft' => '草稿',
|
||||
'published' => '发布中',
|
||||
'hidden' => '隐藏',
|
||||
'deprecated' => '弃用',
|
||||
];
|
||||
$statusTones = [
|
||||
'draft' => 'muted',
|
||||
'published' => 'ok',
|
||||
'hidden' => 'warn',
|
||||
'deprecated' => 'warn',
|
||||
];
|
||||
@endphp
|
||||
|
||||
@extends('admin.layout', [
|
||||
'title' => 'Tstore Admin · 概览',
|
||||
'pageTitle' => '概览',
|
||||
'pageSubtitle' => '先看整体状态,再进入扩展管理、分类管理和版本发布流程。',
|
||||
'title' => 'Tstore Admin · 控制台概览',
|
||||
'pageTitle' => '控制台概览',
|
||||
'pageSubtitle' => '先看整体状态,再继续维护扩展、版本发布和站点接入。',
|
||||
'pageBadge' => 'Overview',
|
||||
])
|
||||
|
||||
@section('content')
|
||||
<div class="grid">
|
||||
<div class="stats">
|
||||
<div class="stat"><span class="label">扩展总数</span><span class="value">{{ $stats['packages'] }}</span><span class="hint">插件 + 主题</span></div>
|
||||
<div class="stat"><span class="label">插件</span><span class="value">{{ $stats['plugins'] }}</span><span class="hint">plugin</span></div>
|
||||
<div class="stat"><span class="label">主题</span><span class="value">{{ $stats['themes'] }}</span><span class="hint">theme</span></div>
|
||||
<div class="stat"><span class="label">版本记录</span><span class="value">{{ $stats['versions'] }}</span><span class="hint">已发布与手动录入版本</span></div>
|
||||
<section class="stats">
|
||||
<div class="stat"><span class="label">扩展总数</span><span class="value">{{ $stats['packages'] }}</span><span class="hint">插件与主题总量</span></div>
|
||||
<div class="stat"><span class="label">插件</span><span class="value">{{ $stats['plugins'] }}</span><span class="hint">plugin package</span></div>
|
||||
<div class="stat"><span class="label">主题</span><span class="value">{{ $stats['themes'] }}</span><span class="hint">theme package</span></div>
|
||||
<div class="stat"><span class="label">版本记录</span><span class="value">{{ $stats['versions'] }}</span><span class="hint">历史发布与手动录入</span></div>
|
||||
<div class="stat"><span class="label">下载总量</span><span class="value">{{ $stats['downloads'] }}</span><span class="hint">累计下载次数</span></div>
|
||||
<div class="stat"><span class="label">分类数</span><span class="value">{{ $stats['categories'] }}</span><span class="hint">插件 / 主题分类</span></div>
|
||||
</div>
|
||||
<div class="stat"><span class="label">分类总数</span><span class="value">{{ $stats['categories'] }}</span><span class="hint">插件与主题分类</span></div>
|
||||
<div class="stat"><span class="label">已注册站点</span><span class="value">{{ $stats['registered_sites'] }}</span><span class="hint">完成注册的客户端</span></div>
|
||||
<div class="stat"><span class="label">在线站点</span><span class="value">{{ $stats['online_sites'] }}</span><span class="hint">最近心跳仍在线</span></div>
|
||||
<div class="stat"><span class="label">追踪用户数</span><span class="value">{{ $stats['tracked_users'] }}</span><span class="hint">所有站点上报总和</span></div>
|
||||
</section>
|
||||
|
||||
<div class="cards">
|
||||
<div class="panel">
|
||||
<div class="split">
|
||||
<section class="panel">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>最近更新的扩展</h2>
|
||||
<p>可以直接进入详情页编辑信息、上传 zip,或查看当前最新稳定版本。</p>
|
||||
<p>这里优先看扩展元数据、分类和当前发布状态,方便继续进入详情页维护。</p>
|
||||
</div>
|
||||
<a class="btn secondary small" href="{{ route('webadmin.packages') }}">查看全部</a>
|
||||
</div>
|
||||
|
||||
<div class="list">
|
||||
@forelse ($recentPackages as $package)
|
||||
<div class="row-card">
|
||||
<article class="row-card">
|
||||
<div class="row-top">
|
||||
<div>
|
||||
<h3>{{ $package->name }}</h3>
|
||||
<p class="muted">{{ $package->type }} · {{ $package->slug }}</p>
|
||||
</div>
|
||||
<span class="chip {{ $package->status === 'published' ? 'ok' : ($package->status === 'draft' ? 'muted' : 'warn') }}">{{ $package->status }}</span>
|
||||
<span class="chip {{ $statusTones[$package->status] ?? 'muted' }}">{{ $statusLabels[$package->status] ?? $package->status }}</span>
|
||||
</div>
|
||||
<p class="muted">{{ $package->summary ?: '暂无摘要' }}</p>
|
||||
<p class="muted">{{ $package->summary ?: '暂无摘要。' }}</p>
|
||||
<div class="tags">
|
||||
@foreach ($package->categories as $category)
|
||||
<span class="chip">{{ $category->name }}</span>
|
||||
@endforeach
|
||||
@if ($package->latestStableVersion)
|
||||
<span class="chip ok">最新稳定版 v{{ $package->latestStableVersion->version }}</span>
|
||||
<span class="chip ok">稳定版 v{{ $package->latestStableVersion->version }}</span>
|
||||
@endif
|
||||
<span class="chip muted">更新于 {{ optional($package->updated_at)->format('Y-m-d H:i') ?: '-' }}</span>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<a class="btn secondary small" href="{{ route('webadmin.packages.show', [$package->type, $package->slug]) }}">查看详情</a>
|
||||
<a class="btn ghost small" href="{{ route('webadmin.packages.show', [$package->type, $package->slug]) }}">查看详情</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@empty
|
||||
<div class="empty">当前还没有扩展数据。</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="panel">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>当前后台能力</h2>
|
||||
<p>这一版已经能覆盖联调闭环里最关键的管理动作。</p>
|
||||
<div class="stack">
|
||||
<section class="panel soft">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>最近版本发布</h2>
|
||||
<p>快速核对最近写入的版本号、兼容区间和稳定状态。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list">
|
||||
<div class="mini-card"><h3>扩展管理</h3><div class="muted">支持创建 package、编辑基础信息、切换发布状态。</div></div>
|
||||
<div class="mini-card"><h3>分类管理</h3><div class="muted">支持新建、编辑、删除分类,并查看分类关联的扩展数量。</div></div>
|
||||
<div class="mini-card"><h3>版本管理</h3><div class="muted">支持手动录入版本、zip 上传发布、删除历史版本。</div></div>
|
||||
<div class="mini-card"><h3>下一步建议</h3><div class="muted">可以继续补截图管理、审核日志、下载统计细分和更严格的发布校验。</div></div>
|
||||
</div>
|
||||
|
||||
<div class="list">
|
||||
@forelse ($recentVersions as $version)
|
||||
<article class="row-card">
|
||||
<div class="row-top">
|
||||
<div>
|
||||
<h3>v{{ $version->version }}</h3>
|
||||
<p class="muted">{{ $version->package?->name ?: '未知扩展' }}</p>
|
||||
</div>
|
||||
<div class="tags" style="margin-top:0">
|
||||
<span class="chip {{ $version->is_stable ? 'ok' : 'warn' }}">{{ $version->is_stable ? '稳定版' : '预发布' }}</span>
|
||||
@if ($version->is_latest)
|
||||
<span class="chip">最新</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="tags">
|
||||
<span class="chip muted">Typecho {{ $version->typecho_min ?: '-' }} ~ {{ $version->typecho_max ?: '-' }}</span>
|
||||
<span class="chip muted">PHP {{ $version->php_min ?: '-' }} ~ {{ $version->php_max ?: '-' }}</span>
|
||||
</div>
|
||||
<p class="muted">发布时间:{{ optional($version->published_at)->format('Y-m-d H:i') ?: '-' }}</p>
|
||||
</article>
|
||||
@empty
|
||||
<div class="empty">当前还没有版本记录。</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel soft">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>最近接入站点</h2>
|
||||
<p>确认插件端是否完成注册、是否在线,以及最近一次同步时间。</p>
|
||||
</div>
|
||||
<a class="btn secondary small" href="{{ route('webadmin.clients') }}">全部站点</a>
|
||||
</div>
|
||||
|
||||
<div class="list">
|
||||
@forelse ($recentClients as $client)
|
||||
<article class="row-card">
|
||||
<div class="row-top">
|
||||
<div>
|
||||
<h3>{{ $client->site_name ?: '未命名站点' }}</h3>
|
||||
<p class="muted">{{ $client->site_url }}</p>
|
||||
</div>
|
||||
<span class="chip {{ $client->is_online ? 'ok' : 'muted' }}">{{ $client->is_online ? '在线' : '离线' }}</span>
|
||||
</div>
|
||||
<div class="tags">
|
||||
<span class="chip">用户 {{ $client->user_count }}</span>
|
||||
<span class="chip muted">Typecho {{ $client->typecho_version ?: '-' }}</span>
|
||||
<span class="chip muted">PHP {{ $client->php_version ?: '-' }}</span>
|
||||
</div>
|
||||
<p class="muted">最近同步:{{ optional($client->last_seen_at)->format('Y-m-d H:i:s') ?: '尚未同步' }}</p>
|
||||
</article>
|
||||
@empty
|
||||
<div class="empty">当前还没有站点完成注册。</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
@php
|
||||
$layoutMode = $layoutMode ?? 'app';
|
||||
$pageTitle = $pageTitle ?? 'Tstore Admin';
|
||||
$pageSubtitle = $pageSubtitle ?? '统一管理扩展、版本与站点接入。';
|
||||
$pageBadge = $pageBadge ?? ($layoutMode === 'guest' ? 'Tstore Service' : '管理后台');
|
||||
$routeType = (string) (request()->route('type') ?: request()->query('type', ''));
|
||||
$siteHost = parse_url(url('/'), PHP_URL_HOST) ?: 'local';
|
||||
@endphp
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
@@ -5,71 +13,298 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ $title ?? 'Tstore Admin' }}</title>
|
||||
<style>
|
||||
:root{--bg:#f3f7fb;--panel:#ffffff;--panel-2:#f7fbff;--text:#16324f;--muted:#607489;--line:#dce6f1;--brand:#2f6ea6;--brand-2:#4f90c8;--ok:#1f8b4c;--warn:#b36b00;--danger:#c14f4f;--shadow:0 12px 30px rgba(16,36,64,.08)}
|
||||
*{box-sizing:border-box}body{margin:0;background:linear-gradient(180deg,#f6f9fc 0,#eef4f9 100%);color:var(--text);font:14px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif}
|
||||
a{color:inherit}.page{display:grid;grid-template-columns:260px minmax(0,1fr);min-height:100vh}
|
||||
.sidebar{background:linear-gradient(180deg,#16324f 0,#1d456b 100%);color:#fff;padding:24px 18px;position:sticky;top:0;height:100vh}
|
||||
.brand{font-size:22px;font-weight:800;letter-spacing:.02em;margin-bottom:6px}.brand-sub{font-size:12px;color:rgba(230,240,255,.72);margin-bottom:24px}
|
||||
.nav{display:grid;gap:10px}.nav a{display:flex;align-items:center;padding:11px 14px;border-radius:14px;text-decoration:none;color:rgba(241,247,255,.9);font-weight:700;background:transparent;border:1px solid transparent}.nav a.active,.nav a:hover{background:rgba(255,255,255,.10);border-color:rgba(255,255,255,.08)}
|
||||
.sidebar-foot{position:absolute;left:18px;right:18px;bottom:20px;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.08);border-radius:16px;padding:14px}.sidebar-foot p{margin:0;color:rgba(235,243,255,.84);font-size:12px;line-height:1.7}.sidebar-foot .muted-mini{display:block;margin-top:8px;color:rgba(224,236,251,.68);font-size:11px}
|
||||
.content{padding:28px}.topbar{display:flex;justify-content:space-between;gap:16px;align-items:center;margin-bottom:20px}.title h1{margin:0 0 4px;font-size:28px;line-height:1.15}.title p{margin:0;color:var(--muted)}
|
||||
.top-actions{display:flex;gap:10px;flex-wrap:wrap}.btn,.btn-link button{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:10px 14px;border-radius:12px;border:1px solid transparent;background:linear-gradient(135deg,var(--brand),var(--brand-2));color:#fff;text-decoration:none;font-weight:700;cursor:pointer;box-shadow:0 10px 22px rgba(47,110,166,.18)}
|
||||
.btn.secondary{background:#fff;color:var(--brand);border-color:#cfe0f1;box-shadow:none}.btn.small,.btn-link button.small{padding:8px 12px;font-size:12px;border-radius:10px}.btn.ghost,.btn-link button.ghost{background:#fff;border-color:var(--line);color:var(--text);box-shadow:none}.btn.danger,.btn-link button.danger{background:#fff2f2;border-color:#f1cccc;color:var(--danger);box-shadow:none}
|
||||
.btn-link{display:inline}.btn-link form{display:inline}
|
||||
.flash{margin-bottom:16px;padding:12px 14px;border-radius:14px;font-weight:700}.flash.success{background:#edf9f0;color:var(--ok);border:1px solid #cfead8}.flash.error{background:#fff4f3;color:var(--danger);border:1px solid #f1d2cf}
|
||||
.panel{background:rgba(255,255,255,.92);border:1px solid var(--line);border-radius:22px;padding:18px;box-shadow:var(--shadow)}
|
||||
.grid{display:grid;gap:18px}.stats{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:16px}.stat{background:linear-gradient(180deg,#fff 0,#f9fbfd 100%);border:1px solid var(--line);border-radius:20px;padding:18px}.stat .label{display:block;font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px}.stat .value{display:block;font-size:28px;font-weight:800;line-height:1.1}.stat .hint{display:block;margin-top:8px;color:var(--muted);font-size:12px}
|
||||
.cards{display:grid;grid-template-columns:1.2fr .8fr;gap:18px}.section-title{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:14px}.section-title h2{margin:0;font-size:18px}.section-title p{margin:0;color:var(--muted);font-size:13px}
|
||||
.table-wrap{overflow:auto;border:1px solid var(--line);border-radius:18px;background:#fff}.table{width:100%;border-collapse:separate;border-spacing:0}.table th,.table td{padding:14px 16px;border-bottom:1px solid #ecf1f6;text-align:left;vertical-align:top}.table th{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;background:#f8fbfd}.table tr:hover td{background:#fbfdff}
|
||||
.chip{display:inline-flex;align-items:center;padding:4px 10px;border-radius:999px;font-size:12px;font-weight:700;background:#edf4fb;color:#2d689d;border:1px solid #d8e7f5}.chip.ok{background:#edf9f0;color:var(--ok);border-color:#cfead8}.chip.warn{background:#fff8eb;color:var(--warn);border-color:#f2dfb1}.chip.muted{background:#f3f6f9;color:#667a90;border-color:#e0e7ef}
|
||||
.stack{display:grid;gap:14px}.mini-card{border:1px solid var(--line);border-radius:18px;padding:16px;background:linear-gradient(180deg,#fff 0,#fbfdff 100%)}.mini-card h3{margin:0 0 6px;font-size:16px}.muted{color:var(--muted)}
|
||||
.toolbar{display:flex;justify-content:space-between;gap:12px;align-items:flex-end;flex-wrap:wrap;margin-bottom:16px}.filters{display:flex;gap:10px;flex-wrap:wrap}.field{display:grid;gap:6px}.field label{font-size:12px;color:var(--muted);font-weight:700}.input,.select,textarea{width:100%;padding:11px 13px;border:1px solid #cfdae8;border-radius:12px;background:#f9fbfd;color:var(--text);outline:none}.input:focus,.select:focus,textarea:focus{border-color:#77aee6;box-shadow:0 0 0 4px rgba(89,156,226,.14);background:#fff}textarea{min-height:96px;resize:vertical}
|
||||
.form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.form-grid.full{grid-template-columns:1fr}.form-actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:14px}
|
||||
.list{display:grid;gap:12px}.row-card{border:1px solid var(--line);border-radius:18px;padding:16px;background:linear-gradient(180deg,#fff 0,#fbfdff 100%)}.row-top{display:flex;justify-content:space-between;gap:16px;align-items:flex-start}.row-top h3{margin:0;font-size:18px}.row-top p{margin:6px 0 0}.tags{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}.split{display:grid;grid-template-columns:1.2fr .8fr;gap:18px}.empty{padding:36px 18px;border:1px dashed var(--line);border-radius:18px;text-align:center;background:#fbfdff;color:var(--muted)}
|
||||
@media (max-width:1200px){.stats{grid-template-columns:repeat(3,minmax(0,1fr))}.cards,.split{grid-template-columns:1fr}}@media (max-width:860px){.page{grid-template-columns:1fr}.sidebar{position:static;height:auto}.content{padding:18px}.stats{grid-template-columns:repeat(2,minmax(0,1fr))}.form-grid{grid-template-columns:1fr}}@media (max-width:560px){.stats{grid-template-columns:1fr}.topbar{flex-direction:column;align-items:flex-start}.filters{width:100%}.field{width:100%}}
|
||||
:root{--bg:#edf4f7;--bg-deep:#102637;--bg-mid:#174c66;--panel:#ffffff;--panel-soft:#f7fbfc;--line:#d6e3ea;--ink:#132736;--ink-soft:#607484;--accent:#1c7d85;--accent-deep:#0f5060;--accent-soft:#e6f4f5;--warm:#c49152;--ok:#1c8a56;--warn:#b87218;--danger:#c05050;--shadow-lg:0 28px 58px rgba(10,26,38,.12);--shadow-md:0 16px 34px rgba(13,29,40,.08)}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;min-height:100vh;background:radial-gradient(circle at top left,rgba(196,145,82,.14),transparent 24%),radial-gradient(circle at bottom right,rgba(28,125,133,.12),transparent 28%),linear-gradient(180deg,#f7fafb 0,#edf4f7 100%);color:var(--ink);font:14px/1.65 "Segoe UI Variable","PingFang SC","Microsoft YaHei UI",sans-serif}
|
||||
a{color:inherit;text-decoration:none}
|
||||
button,input,select,textarea{font:inherit}
|
||||
code{padding:2px 6px;border-radius:8px;background:#edf4f6;color:var(--accent-deep)}
|
||||
.page{display:grid;grid-template-columns:280px minmax(0,1fr);min-height:100vh}
|
||||
.sidebar{position:sticky;top:0;height:100vh;overflow:hidden;padding:24px 18px 22px;background:linear-gradient(180deg,#102536 0,#143b52 48%,#1c6f79 100%);color:#fff}
|
||||
.sidebar:before{content:'';position:absolute;inset:0;background:linear-gradient(rgba(255,255,255,.06) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.05) 1px,transparent 1px);background-size:30px 30px;mask-image:linear-gradient(180deg,rgba(0,0,0,.38),transparent 82%)}
|
||||
.sidebar > *{position:relative;z-index:1}
|
||||
.sidebar-top{display:flex;align-items:flex-start;justify-content:space-between;gap:14px}
|
||||
.brand{display:flex;align-items:center;gap:14px;min-width:0}
|
||||
.brand-mark{display:grid;place-items:center;width:48px;height:48px;border-radius:16px;background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.14);font-size:18px;font-weight:900;letter-spacing:.08em}
|
||||
.brand-copy strong{display:block;font-size:21px;line-height:1.1}
|
||||
.brand-copy span{display:block;margin-top:5px;color:rgba(232,240,244,.7);font-size:12px;letter-spacing:.16em;text-transform:uppercase}
|
||||
.sidebar-toggle{display:none;align-items:center;justify-content:center;min-height:42px;padding:0 14px;border-radius:14px;border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.08);color:#fff;font-weight:900;cursor:pointer}
|
||||
.sidebar-body{display:block}
|
||||
.sidebar-group{margin-top:28px}
|
||||
.sidebar-label{display:block;margin-bottom:12px;color:rgba(228,238,243,.66);font-size:11px;font-weight:900;letter-spacing:.16em;text-transform:uppercase}
|
||||
.nav{display:grid;gap:8px}
|
||||
.nav a{display:flex;align-items:center;gap:10px;min-height:46px;padding:0 14px;border-radius:16px;color:rgba(240,246,249,.88);font-weight:800;border:1px solid transparent;background:rgba(255,255,255,.03);transition:background .18s ease,border-color .18s ease,transform .18s ease}
|
||||
.nav a:hover,.nav a.active{background:rgba(255,255,255,.12);border-color:rgba(255,255,255,.12);transform:translateX(2px)}
|
||||
.sidebar-meta{display:grid;gap:10px;margin-top:28px}
|
||||
.meta-card{padding:14px 16px;border-radius:18px;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.10)}
|
||||
.meta-label{display:block;font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:rgba(233,240,244,.68)}
|
||||
.meta-value{display:block;margin-top:6px;font-size:16px;font-weight:900;line-height:1.35;word-break:break-word}
|
||||
.content-shell{min-width:0}
|
||||
.content-inner{width:min(1600px,100%);margin:0 auto;padding:30px 32px 42px}
|
||||
.topbar{display:flex;justify-content:space-between;gap:18px;align-items:flex-end;margin-bottom:22px}
|
||||
.page-badge{display:inline-flex;align-items:center;padding:6px 12px;border-radius:999px;background:var(--accent-soft);color:var(--accent-deep);font-size:11px;font-weight:900;letter-spacing:.16em;text-transform:uppercase}
|
||||
.headline-block h1{margin:14px 0 0;font-size:38px;line-height:1.02;letter-spacing:-.05em}
|
||||
.headline-block p{margin:10px 0 0;color:var(--ink-soft);max-width:760px}
|
||||
.top-actions{display:flex;gap:10px;flex-wrap:wrap}
|
||||
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:44px;padding:0 16px;border-radius:14px;border:1px solid transparent;background:linear-gradient(135deg,var(--accent-deep),var(--accent));color:#fff;font-weight:900;cursor:pointer;box-shadow:0 14px 28px rgba(11,79,91,.18)}
|
||||
.btn.secondary{background:#fff;color:var(--accent-deep);border-color:#d1e4e7;box-shadow:none}
|
||||
.btn.ghost{background:rgba(255,255,255,.76);color:var(--ink);border-color:var(--line);box-shadow:none}
|
||||
.btn.danger{background:#fff1f1;color:var(--danger);border-color:#f0cccc;box-shadow:none}
|
||||
.btn.small{min-height:38px;padding:0 12px;border-radius:12px;font-size:12px}
|
||||
.inline-form{display:inline}
|
||||
.flash-stack{display:grid;gap:12px;margin-bottom:18px}
|
||||
.flash{padding:12px 14px;border-radius:16px;border:1px solid var(--line);background:#fff;font-weight:800;box-shadow:var(--shadow-md)}
|
||||
.flash.success{background:#edf9f1;border-color:#d2eadb;color:var(--ok)}
|
||||
.flash.error{background:#fff4f3;border-color:#f1d5d2;color:var(--danger)}
|
||||
.grid{display:grid;gap:20px}
|
||||
.stack{display:grid;gap:20px}
|
||||
.split{display:grid;grid-template-columns:minmax(0,1.18fr) minmax(320px,.82fr);gap:20px}
|
||||
.panel{padding:22px;border-radius:28px;border:1px solid var(--line);background:linear-gradient(180deg,rgba(255,255,255,.96),rgba(247,251,252,.98));box-shadow:var(--shadow-lg)}
|
||||
.panel.soft{background:linear-gradient(180deg,#fbfdfe 0,#f3f8fa 100%)}
|
||||
.section-title{display:flex;justify-content:space-between;gap:14px;align-items:flex-end;margin-bottom:18px}
|
||||
.section-title h2{margin:0;font-size:24px;line-height:1.1;letter-spacing:-.03em}
|
||||
.section-title p{margin:8px 0 0;color:var(--ink-soft)}
|
||||
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px}
|
||||
.stat{position:relative;overflow:hidden;padding:18px;border-radius:22px;border:1px solid var(--line);background:linear-gradient(180deg,#fff 0,#f7fbfc 100%);box-shadow:var(--shadow-md)}
|
||||
.stat:after{content:'';position:absolute;right:-28px;top:-24px;width:96px;height:96px;border-radius:28px;background:linear-gradient(135deg,rgba(28,125,133,.12),rgba(28,125,133,0))}
|
||||
.stat .label{display:block;font-size:11px;font-weight:900;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-soft)}
|
||||
.stat .value{display:block;margin-top:10px;font-size:30px;line-height:1.05;font-weight:900}
|
||||
.stat .hint{display:block;margin-top:8px;color:var(--ink-soft);font-size:13px}
|
||||
.table-wrap{overflow:auto;border:1px solid var(--line);border-radius:22px;background:#fff}
|
||||
.table{width:100%;border-collapse:separate;border-spacing:0}
|
||||
.table th,.table td{padding:15px 16px;border-bottom:1px solid #ebf1f4;text-align:left;vertical-align:top}
|
||||
.table th{background:#f7fafb;color:var(--ink-soft);font-size:11px;font-weight:900;letter-spacing:.14em;text-transform:uppercase}
|
||||
.table tr:last-child td{border-bottom:none}
|
||||
.table tr:hover td{background:#fbfdfe}
|
||||
.list{display:grid;gap:14px}
|
||||
.row-card{padding:18px;border-radius:22px;border:1px solid var(--line);background:linear-gradient(180deg,#fff 0,#f8fbfc 100%);box-shadow:var(--shadow-md)}
|
||||
.row-top{display:flex;justify-content:space-between;gap:16px;align-items:flex-start}
|
||||
.row-top h3{margin:0;font-size:18px;line-height:1.18}
|
||||
.row-top p{margin:6px 0 0}
|
||||
.muted{color:var(--ink-soft)}
|
||||
.tags{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}
|
||||
.chip{display:inline-flex;align-items:center;padding:6px 11px;border-radius:999px;border:1px solid #d7e7eb;background:#eef6f7;color:#215b68;font-size:12px;font-weight:900}
|
||||
.chip.ok{background:#edf9f1;border-color:#d2eadb;color:var(--ok)}
|
||||
.chip.warn{background:#fff7ea;border-color:#eed8b7;color:var(--warn)}
|
||||
.chip.muted{background:#f3f6f8;border-color:#e1e8ec;color:#667b8b}
|
||||
.toolbar{display:flex;justify-content:space-between;gap:14px;align-items:flex-end;flex-wrap:wrap}
|
||||
.filters{display:flex;gap:12px;flex-wrap:wrap;flex:1 1 auto}
|
||||
.field{display:grid;gap:7px}
|
||||
.field label{font-size:12px;font-weight:900;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-soft)}
|
||||
.input,.select,textarea{width:100%;min-width:0;padding:12px 14px;border-radius:16px;border:1px solid #cfdee5;background:#fbfcfd;color:var(--ink);outline:none;transition:border-color .18s ease,box-shadow .18s ease,background .18s ease}
|
||||
.input:focus,.select:focus,textarea:focus{background:#fff;border-color:#72acb6;box-shadow:0 0 0 4px rgba(28,125,133,.12)}
|
||||
textarea{min-height:110px;resize:vertical}
|
||||
.form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
|
||||
.form-grid.compact{grid-template-columns:repeat(3,minmax(0,1fr))}
|
||||
.form-actions{display:flex;gap:10px;flex-wrap:wrap}
|
||||
.checkbox-row{display:flex;gap:14px;flex-wrap:wrap;align-items:center}
|
||||
.checkbox-row label{display:inline-flex;align-items:center;gap:8px;color:var(--ink);font-weight:700}
|
||||
.empty{padding:36px 18px;border-radius:22px;border:1px dashed #c8d7dd;background:#fbfdfe;color:var(--ink-soft);text-align:center}
|
||||
.kv-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}
|
||||
.kv-card{padding:16px;border-radius:18px;border:1px solid var(--line);background:#fbfdfe}
|
||||
.kv-label{display:block;font-size:11px;font-weight:900;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-soft)}
|
||||
.kv-value{display:block;margin-top:6px;font-size:15px;font-weight:800;line-height:1.45;word-break:break-word}
|
||||
.entity-head{display:flex;gap:16px;align-items:flex-start;margin-bottom:18px}
|
||||
.entity-icon{display:grid;place-items:center;width:74px;height:74px;border-radius:24px;border:1px solid var(--line);background:linear-gradient(135deg,#eef6f7,#dce9ec);color:var(--accent-deep);font-size:24px;font-weight:900;overflow:hidden}
|
||||
.entity-icon img{width:100%;height:100%;object-fit:cover}
|
||||
.entity-copy h2{margin:0;font-size:24px;line-height:1.08}
|
||||
.entity-copy p{margin:10px 0 0;color:var(--ink-soft)}
|
||||
.preview-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}
|
||||
.preview-card{padding:10px;border-radius:18px;border:1px solid var(--line);background:#fff;box-shadow:var(--shadow-md)}
|
||||
.preview-shot{margin:0;border-radius:16px;overflow:hidden;border:1px solid var(--line);background:#fff}
|
||||
.preview-shot img{width:100%;height:124px;object-fit:cover}
|
||||
.preview-shot figcaption{padding:10px 12px;color:var(--ink-soft);font-size:13px}
|
||||
.preview-tools{display:flex;justify-content:space-between;gap:10px;align-items:center;flex-wrap:wrap;margin-top:10px}
|
||||
nav[role="navigation"]{margin-top:20px}
|
||||
nav[role="navigation"] > div:first-child{display:none}
|
||||
nav[role="navigation"] > div:last-child{display:flex;justify-content:center}
|
||||
nav[role="navigation"] a,nav[role="navigation"] span{display:inline-flex;align-items:center;justify-content:center;min-width:40px;height:40px;margin:0 4px;padding:0 14px;border-radius:999px;border:1px solid var(--line);background:#fff;color:var(--ink);box-shadow:0 10px 20px rgba(16,35,49,.06)}
|
||||
nav[role="navigation"] span[aria-current="page"]{background:linear-gradient(135deg,var(--accent-deep),var(--accent));border-color:transparent;color:#fff}
|
||||
nav[role="navigation"] svg{display:none}
|
||||
.guest-mode{background:radial-gradient(circle at top left,rgba(196,145,82,.18),transparent 26%),radial-gradient(circle at bottom right,rgba(28,125,133,.16),transparent 28%),linear-gradient(180deg,#f6f9fa 0,#eef4f6 100%)}
|
||||
.guest-shell{min-height:100vh;display:grid;place-items:center;padding:24px}
|
||||
.guest-stage{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(420px,.95fr);width:min(1260px,100%);border-radius:34px;overflow:hidden;border:1px solid #d5e3e8;box-shadow:0 32px 72px rgba(12,26,37,.14);background:#fff}
|
||||
.guest-hero{padding:34px;background:linear-gradient(145deg,#102536 0,#16445c 52%,#1c7d85 100%);color:#fff;position:relative;overflow:hidden}
|
||||
.guest-hero:after{content:'';position:absolute;right:-70px;bottom:-90px;width:220px;height:220px;border-radius:56px;background:linear-gradient(135deg,rgba(255,255,255,.18),rgba(255,255,255,0));transform:rotate(16deg)}
|
||||
.guest-hero > *{position:relative;z-index:1}
|
||||
.guest-hero .page-badge{background:rgba(255,255,255,.12);color:#fff}
|
||||
.guest-hero h1{margin:18px 0 0;font-size:44px;line-height:1.02;letter-spacing:-.05em}
|
||||
.guest-hero p{margin:12px 0 0;color:rgba(230,239,244,.84);max-width:560px}
|
||||
.auth-points{display:grid;gap:12px;margin-top:28px}
|
||||
.auth-point{padding:16px 18px;border-radius:20px;background:rgba(255,255,255,.09);border:1px solid rgba(255,255,255,.10)}
|
||||
.auth-point strong{display:block;font-size:16px}
|
||||
.auth-point span{display:block;margin-top:6px;color:rgba(230,239,244,.78)}
|
||||
.guest-note{margin-top:20px;color:rgba(230,239,244,.78)}
|
||||
.guest-panel{padding:34px;background:linear-gradient(180deg,#fff 0,#f7fbfc 100%)}
|
||||
.login-sheet{display:grid;gap:18px}
|
||||
@media (max-width:1240px){.page,.guest-stage,.split{grid-template-columns:1fr}.sidebar{position:static;height:auto;overflow:visible;padding:18px}.sidebar-toggle{display:inline-flex}.sidebar-body{display:none;width:100%;padding-top:18px}.sidebar.is-open .sidebar-body{display:block}.content-inner{padding:22px}.guest-hero,.guest-panel{padding:26px}}
|
||||
@media (max-width:860px){.topbar{flex-direction:column;align-items:flex-start}.form-grid,.form-grid.compact,.kv-grid,.preview-grid{grid-template-columns:1fr}.stats{grid-template-columns:repeat(2,minmax(0,1fr))}.filters,.form-actions,.checkbox-row{width:100%}.filters{flex-direction:column}.field{width:100%}}
|
||||
@media (max-width:560px){.guest-shell{padding:12px}.guest-hero h1,.headline-block h1{font-size:34px}.stats{grid-template-columns:1fr}.panel,.guest-hero,.guest-panel{padding:20px}.content-inner{padding:16px}.top-actions{width:100%}.top-actions > *{flex:1 1 auto}}
|
||||
</style>
|
||||
@stack('styles')
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<aside class="sidebar">
|
||||
<div class="brand">Tstore Admin</div>
|
||||
<div class="brand-sub">Laravel management console</div>
|
||||
<nav class="nav">
|
||||
<a href="{{ route('webadmin.home') }}" class="{{ request()->routeIs('webadmin.home') ? 'active' : '' }}">概览</a>
|
||||
<a href="{{ route('webadmin.packages') }}" class="{{ request()->routeIs('webadmin.packages*') && !request('type') ? 'active' : '' }}">扩展管理</a>
|
||||
<a href="{{ route('webadmin.categories') }}" class="{{ request()->routeIs('webadmin.categories*') ? 'active' : '' }}">分类管理</a>
|
||||
<a href="{{ route('webadmin.packages', ['type' => 'plugin']) }}" class="{{ request()->routeIs('webadmin.packages*') && request('type') === 'plugin' ? 'active' : '' }}">插件列表</a>
|
||||
<a href="{{ route('webadmin.packages', ['type' => 'theme']) }}" class="{{ request()->routeIs('webadmin.packages*') && request('type') === 'theme' ? 'active' : '' }}">主题列表</a>
|
||||
</nav>
|
||||
<div class="sidebar-foot">
|
||||
<p>当前后台已接入账号登录,并支持分类维护、包管理、zip 上传发布、手动版本管理和版本删除。适合先维护 package 与分类,再进入详情页发布 zip 做联调。</p>
|
||||
<span class="muted-mini">建议:先登录后台账号,维护 package 和分类,再进入详情页完成 zip 发布。</span>
|
||||
</div>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<div class="topbar">
|
||||
<div class="title">
|
||||
<h1>{{ $pageTitle ?? 'Tstore Admin' }}</h1>
|
||||
<p>{{ $pageSubtitle ?? '统一管理扩展、分类、版本与发布流程。' }}</p>
|
||||
</div>
|
||||
<div class="top-actions">
|
||||
<a class="btn secondary" href="{{ route('webadmin.home') }}">后台首页</a>
|
||||
<form method="post" action="{{ route('webadmin.logout') }}">@csrf<button class="btn ghost">退出</button></form>
|
||||
</div>
|
||||
</div>
|
||||
<body @class(['guest-mode' => $layoutMode === 'guest'])>
|
||||
@if ($layoutMode === 'guest')
|
||||
<div class="guest-shell">
|
||||
<div class="guest-stage">
|
||||
<section class="guest-hero">
|
||||
<a class="brand" href="{{ route('storefront.home') }}">
|
||||
<span class="brand-mark">TS</span>
|
||||
<span class="brand-copy">
|
||||
<strong>{{ $storeSettings['site_name'] ?? config('app.name', 'Tstore') }}</strong>
|
||||
<span>Service Console</span>
|
||||
</span>
|
||||
</a>
|
||||
<span class="page-badge">{{ $pageBadge }}</span>
|
||||
<h1>{{ $pageTitle }}</h1>
|
||||
<p>{{ $pageSubtitle }}</p>
|
||||
<div class="auth-points">
|
||||
<div class="auth-point">
|
||||
<strong>公开展示与后台维护拆开</strong>
|
||||
<span>前台负责扩展展示,后台负责发布、分类和站点接入。</span>
|
||||
</div>
|
||||
<div class="auth-point">
|
||||
<strong>登录后统一处理服务端数据</strong>
|
||||
<span>版本、分类、站点注册状态和后台发布入口都集中在一个控制台里。</span>
|
||||
</div>
|
||||
<div class="auth-point">
|
||||
<strong>默认先进入本地控制台</strong>
|
||||
<span>如果你已经初始化管理员账号,可以直接登录继续维护服务端。</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="guest-note">公开站点:<a href="{{ route('storefront.home') }}">返回首页</a></div>
|
||||
</section>
|
||||
<main class="guest-panel">
|
||||
@if (session('success') || session('error') || $errors->any())
|
||||
<div class="flash-stack">
|
||||
@if (session('success'))
|
||||
<div class="flash success">{{ session('success') }}</div>
|
||||
@endif
|
||||
@if (session('error'))
|
||||
<div class="flash error">{{ session('error') }}</div>
|
||||
@endif
|
||||
@if ($errors->any())
|
||||
<div class="flash error">{{ $errors->first() }}</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (session('success'))
|
||||
<div class="flash success">{{ session('success') }}</div>
|
||||
@endif
|
||||
@if (session('error'))
|
||||
<div class="flash error">{{ session('error') }}</div>
|
||||
@endif
|
||||
@if ($errors->any())
|
||||
<div class="flash error">{{ $errors->first() }}</div>
|
||||
@endif
|
||||
@yield('content')
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="page">
|
||||
<aside class="sidebar" data-sidebar>
|
||||
<div class="sidebar-top">
|
||||
<a class="brand" href="{{ route('webadmin.home') }}">
|
||||
<span class="brand-mark">TS</span>
|
||||
<span class="brand-copy">
|
||||
<strong>{{ $storeSettings['site_name'] ?? config('app.name', 'Tstore') }}</strong>
|
||||
<span>Admin Console</span>
|
||||
</span>
|
||||
</a>
|
||||
<button class="sidebar-toggle" type="button" data-sidebar-toggle aria-expanded="false" aria-controls="admin-sidebar-body">菜单</button>
|
||||
</div>
|
||||
|
||||
@yield('content')
|
||||
</main>
|
||||
</div>
|
||||
<div class="sidebar-body" id="admin-sidebar-body">
|
||||
<div class="sidebar-group">
|
||||
<span class="sidebar-label">控制台</span>
|
||||
<nav class="nav">
|
||||
<a href="{{ route('webadmin.home') }}" class="{{ request()->routeIs('webadmin.home') ? 'active' : '' }}">概览</a>
|
||||
<a href="{{ route('webadmin.packages') }}" class="{{ request()->routeIs('webadmin.packages*') && $routeType === '' ? 'active' : '' }}">扩展管理</a>
|
||||
<a href="{{ route('webadmin.categories') }}" class="{{ request()->routeIs('webadmin.categories*') ? 'active' : '' }}">分类管理</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-group">
|
||||
<span class="sidebar-label">按类型</span>
|
||||
<nav class="nav">
|
||||
<a href="{{ route('webadmin.packages', ['type' => 'plugin']) }}" class="{{ request()->routeIs('webadmin.packages*') && $routeType === 'plugin' ? 'active' : '' }}">插件列表</a>
|
||||
<a href="{{ route('webadmin.packages', ['type' => 'theme']) }}" class="{{ request()->routeIs('webadmin.packages*') && $routeType === 'theme' ? 'active' : '' }}">主题列表</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-meta">
|
||||
<div class="meta-card">
|
||||
<span class="meta-label">当前用户</span>
|
||||
<span class="meta-value">{{ auth()->user()?->email ?: '管理员' }}</span>
|
||||
</div>
|
||||
<div class="meta-card">
|
||||
<span class="meta-label">运行环境</span>
|
||||
<span class="meta-value">{{ strtoupper(app()->environment()) }}</span>
|
||||
</div>
|
||||
<div class="meta-card">
|
||||
<span class="meta-label">站点域名</span>
|
||||
<span class="meta-value">{{ $siteHost }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="content-shell">
|
||||
<div class="content-inner">
|
||||
<div class="topbar">
|
||||
<div class="headline-block">
|
||||
<span class="page-badge">{{ $pageBadge }}</span>
|
||||
<h1>{{ $pageTitle }}</h1>
|
||||
<p>{{ $pageSubtitle }}</p>
|
||||
</div>
|
||||
<div class="top-actions">
|
||||
<a class="btn secondary" href="{{ route('storefront.home') }}">公开前台</a>
|
||||
<a class="btn ghost" href="{{ route('webadmin.account') }}">账户安全</a>
|
||||
<a class="btn ghost" href="{{ route('webadmin.settings') }}">站点设置</a>
|
||||
<a class="btn ghost" href="{{ route('webadmin.clients') }}">站点接入</a>
|
||||
<form class="inline-form" method="post" action="{{ route('webadmin.logout') }}">
|
||||
@csrf
|
||||
<button class="btn ghost" type="submit">退出登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (session('success') || session('error') || $errors->any())
|
||||
<div class="flash-stack">
|
||||
@if (session('success'))
|
||||
<div class="flash success">{{ session('success') }}</div>
|
||||
@endif
|
||||
@if (session('error'))
|
||||
<div class="flash error">{{ session('error') }}</div>
|
||||
@endif
|
||||
@if ($errors->any())
|
||||
<div class="flash error">{{ $errors->first() }}</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@yield('content')
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@endif
|
||||
<script>
|
||||
(() => {
|
||||
const sidebar = document.querySelector('[data-sidebar]');
|
||||
const toggle = document.querySelector('[data-sidebar-toggle]');
|
||||
|
||||
if (!sidebar || !toggle) {
|
||||
return;
|
||||
}
|
||||
|
||||
const syncSidebarState = () => {
|
||||
if (window.innerWidth > 1240) {
|
||||
sidebar.classList.remove('is-open');
|
||||
toggle.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
};
|
||||
|
||||
toggle.addEventListener('click', () => {
|
||||
const open = sidebar.classList.toggle('is-open');
|
||||
toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
});
|
||||
|
||||
window.addEventListener('resize', syncSidebarState);
|
||||
syncSidebarState();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
@extends('admin.layout', [
|
||||
'title' => 'Tstore Admin · 后台登录',
|
||||
'pageTitle' => '后台登录',
|
||||
'pageSubtitle' => '使用后台账号密码登录,不再直接依赖 token。',
|
||||
'pageSubtitle' => '使用管理员账号进入服务端控制台,继续维护扩展发布、分类和站点接入。',
|
||||
'pageBadge' => 'Admin Login',
|
||||
'layoutMode' => 'guest',
|
||||
])
|
||||
|
||||
@section('content')
|
||||
<div class="panel" style="max-width:620px">
|
||||
<div class="login-sheet">
|
||||
<form method="post" action="{{ route('webadmin.login.submit') }}" class="grid">
|
||||
@csrf
|
||||
<input type="hidden" name="redirect" value="{{ $redirect }}">
|
||||
|
||||
<div class="field">
|
||||
<label>邮箱</label>
|
||||
<input class="input" type="email" name="email" value="{{ old('email', 'admin@tstore.local') }}" placeholder="admin@tstore.local" required>
|
||||
<input class="input" type="email" name="email" value="{{ old('email') }}" placeholder="输入管理员邮箱" required>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>密码</label>
|
||||
<input class="input" type="password" name="password" placeholder="输入后台密码" required>
|
||||
</div>
|
||||
<label><input type="checkbox" name="remember" value="1"> 记住登录状态</label>
|
||||
|
||||
<div class="checkbox-row">
|
||||
<label><input type="checkbox" name="remember" value="1" @checked(old('remember'))> 保持登录状态</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn" type="submit">进入后台</button>
|
||||
</div>
|
||||
<div class="mini-card">
|
||||
<h3>默认管理员</h3>
|
||||
<div class="muted">邮箱:admin@tstore.local</div>
|
||||
<div class="muted">密码:Admin@123456</div>
|
||||
<div class="muted">可以在 .env 中通过 ADMIN_EMAIL / ADMIN_PASSWORD 修改。</div>
|
||||
<a class="btn ghost" href="{{ route('storefront.home') }}">返回前台</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,78 +1,87 @@
|
||||
@extends('admin.layout', [
|
||||
'title' => 'Tstore Admin · 插件 / 主题管理',
|
||||
'pageTitle' => $filters['type'] === 'plugin' ? '插件列表' : ($filters['type'] === 'theme' ? '主题列表' : '扩展列表'),
|
||||
'pageSubtitle' => $filters['type'] === 'plugin'
|
||||
? '查看、筛选、维护和发布插件,统一处理插件版本与状态。'
|
||||
: ($filters['type'] === 'theme'
|
||||
? '查看、筛选、维护和发布主题,统一处理主题版本与状态。'
|
||||
: '查看全部插件与主题,统一管理扩展列表、详情页与发布版本。'),
|
||||
])
|
||||
|
||||
@php
|
||||
$statusLabels = [
|
||||
'draft' => '草稿',
|
||||
'published' => '发布中',
|
||||
'hidden' => '隐藏',
|
||||
'deprecated' => '弃用',
|
||||
];
|
||||
$statusTones = [
|
||||
'draft' => 'muted',
|
||||
'published' => 'ok',
|
||||
'hidden' => 'warn',
|
||||
'deprecated' => 'warn',
|
||||
];
|
||||
$currentType = $filters['type'] ?? '';
|
||||
$entityLabel = $currentType === 'plugin' ? '插件' : ($currentType === 'theme' ? '主题' : '扩展');
|
||||
$entityPluralLabel = $currentType === 'plugin' ? '插件列表' : ($currentType === 'theme' ? '主题列表' : '扩展列表');
|
||||
$newEntityLabel = $currentType === 'plugin' ? '新建插件' : ($currentType === 'theme' ? '新建主题' : '新建扩展');
|
||||
$filterHint = $currentType === 'plugin' ? '按类型、状态和关键词快速定位插件。'
|
||||
: ($currentType === 'theme' ? '按类型、状态和关键词快速定位主题。' : '按类型、状态和关键词快速定位扩展。');
|
||||
$listHint = $currentType === 'plugin' ? '支持查看插件详情、编辑元数据、上传 zip 发布版本,以及切换插件状态。'
|
||||
: ($currentType === 'theme' ? '支持查看主题详情、编辑元数据、上传 zip 发布版本,以及切换主题状态。'
|
||||
: '支持查看详情、编辑元数据、上传 zip 发布版本,以及切换扩展状态。');
|
||||
$createHint = $currentType === 'plugin' ? '先创建插件 package,再进入详情页补充版本,或直接上传 zip 发布。'
|
||||
: ($currentType === 'theme' ? '先创建主题 package,再进入详情页补充版本,或直接上传 zip 发布。'
|
||||
: '先创建 package,再进入详情页补充版本,或直接上传 zip 发布。');
|
||||
$emptyHint = '当前还没有' . $entityLabel . '。';
|
||||
$newEntityLabel = '新建' . $entityLabel;
|
||||
$currentStatusLabel = $filters['status'] !== '' ? ($statusLabels[$filters['status']] ?? $filters['status']) : '全部状态';
|
||||
@endphp
|
||||
|
||||
@extends('admin.layout', [
|
||||
'title' => 'Tstore Admin · 扩展管理',
|
||||
'pageTitle' => $entityPluralLabel,
|
||||
'pageSubtitle' => '统一维护扩展元数据、分类关联、发布状态与版本入口。',
|
||||
'pageBadge' => 'Packages',
|
||||
])
|
||||
|
||||
@section('content')
|
||||
<div class="grid">
|
||||
<div class="panel">
|
||||
<section class="stats">
|
||||
<div class="stat"><span class="label">当前结果</span><span class="value">{{ $packages->total() }}</span><span class="hint">符合筛选条件的{{ $entityLabel }}</span></div>
|
||||
<div class="stat"><span class="label">当前类型</span><span class="value">{{ $currentType === '' ? '全部' : strtoupper($currentType) }}</span><span class="hint">{{ $entityLabel }}视角</span></div>
|
||||
<div class="stat"><span class="label">当前状态</span><span class="value">{{ $currentStatusLabel }}</span><span class="hint">已应用状态筛选</span></div>
|
||||
<div class="stat"><span class="label">页码</span><span class="value">{{ $packages->currentPage() }}/{{ max($packages->lastPage(), 1) }}</span><span class="hint">分页浏览</span></div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>筛选与查询</h2>
|
||||
<p>{{ $filterHint }}</p>
|
||||
<p>先缩小范围,再进入详情页处理版本发布或资料修改。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="get" class="toolbar">
|
||||
<div class="filters">
|
||||
<div class="field">
|
||||
<label>类型</label>
|
||||
<select name="type" class="select">
|
||||
<option value="">全部</option>
|
||||
<option value="plugin" @selected($filters['type']==='plugin')>插件</option>
|
||||
<option value="theme" @selected($filters['type']==='theme')>主题</option>
|
||||
<option value="plugin" @selected($filters['type'] === 'plugin')>插件</option>
|
||||
<option value="theme" @selected($filters['type'] === 'theme')>主题</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>状态</label>
|
||||
<select name="status" class="select">
|
||||
<option value="">全部</option>
|
||||
<option value="draft" @selected($filters['status']==='draft')>draft</option>
|
||||
<option value="published" @selected($filters['status']==='published')>published</option>
|
||||
<option value="hidden" @selected($filters['status']==='hidden')>hidden</option>
|
||||
<option value="deprecated" @selected($filters['status']==='deprecated')>deprecated</option>
|
||||
@foreach ($statusLabels as $status => $label)
|
||||
<option value="{{ $status }}" @selected($filters['status'] === $status)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="field" style="min-width:280px">
|
||||
<div class="field" style="flex:1 1 280px">
|
||||
<label>关键词</label>
|
||||
<input class="input" type="text" name="keyword" value="{{ $filters['keyword'] }}" placeholder="搜索 name / slug / summary">
|
||||
<input class="input" type="text" name="keyword" value="{{ $filters['keyword'] }}" placeholder="搜索名称、slug 或摘要">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="btn" type="submit">筛选</button>
|
||||
<button class="btn" type="submit">应用筛选</button>
|
||||
<a class="btn secondary" href="{{ route('webadmin.packages') }}">重置</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="split">
|
||||
<div class="panel">
|
||||
<div class="stack">
|
||||
<section class="panel">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>{{ $entityPluralLabel }}</h2>
|
||||
<p>{{ $listHint }}</p>
|
||||
<p>保持列表信息密度,重点展示状态、当前版本、分类和最近更新时间。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="table">
|
||||
<thead>
|
||||
@@ -82,6 +91,7 @@
|
||||
<th>状态</th>
|
||||
<th>最新版本</th>
|
||||
<th>分类</th>
|
||||
<th>更新时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -91,108 +101,134 @@
|
||||
<td>
|
||||
<strong>{{ $package->name }}</strong>
|
||||
<div class="muted">{{ $package->slug }}</div>
|
||||
<div class="muted">{{ $package->summary ?: '暂无摘要' }}</div>
|
||||
<div class="muted">{{ $package->summary ?: '暂无摘要。' }}</div>
|
||||
</td>
|
||||
<td>{{ $package->type }}</td>
|
||||
<td><span class="chip {{ $package->status === 'published' ? 'ok' : ($package->status === 'draft' ? 'muted' : 'warn') }}">{{ $package->status }}</span></td>
|
||||
<td>{{ strtoupper($package->type) }}</td>
|
||||
<td><span class="chip {{ $statusTones[$package->status] ?? 'muted' }}">{{ $statusLabels[$package->status] ?? $package->status }}</span></td>
|
||||
<td>{{ $package->latestStableVersion?->version ?: ($package->latest_version ?: '-') }}</td>
|
||||
<td>
|
||||
<div class="tags">
|
||||
@foreach ($package->categories as $category)
|
||||
<span class="chip">{{ $category->slug }}</span>
|
||||
@endforeach
|
||||
@forelse ($package->categories as $category)
|
||||
<span class="chip">{{ $category->name }}</span>
|
||||
@empty
|
||||
<span class="muted">未分类</span>
|
||||
@endforelse
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ optional($package->updated_at)->format('Y-m-d H:i') ?: '-' }}</td>
|
||||
<td>
|
||||
<div class="form-actions">
|
||||
<a class="btn secondary small" href="{{ route('webadmin.packages.show', [$package->type, $package->slug]) }}">详情 / 发布</a>
|
||||
<a class="btn ghost small" href="{{ route('webadmin.packages.show', [$package->type, $package->slug]) }}">详情</a>
|
||||
<form method="post" action="{{ route('webadmin.packages.status', [$package->type, $package->slug]) }}">
|
||||
@csrf @method('PATCH')
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
<input type="hidden" name="status" value="{{ $package->status === 'published' ? 'hidden' : 'published' }}">
|
||||
<button class="btn ghost small" type="submit">{{ $package->status === 'published' ? '隐藏' : '发布' }}</button>
|
||||
<button class="btn secondary small" type="submit">{{ $package->status === 'published' ? '隐藏' : '发布' }}</button>
|
||||
</form>
|
||||
<form method="post" action="{{ route('webadmin.packages.destroy', [$package->type, $package->slug]) }}" onsubmit="return confirm('确认删除这个{{ $entityLabel }}吗?版本、截图和分类关联都会一起删除。');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button class="btn danger small" type="submit">删除</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="6"><div class="empty">{{ $emptyHint }}</div></td></tr>
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<div class="empty">当前还没有{{ $entityLabel }}。</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="margin-top:14px">{{ $packages->links() }}</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
{{ $packages->links() }}
|
||||
</section>
|
||||
|
||||
<section class="panel soft">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>{{ $newEntityLabel }}</h2>
|
||||
<p>{{ $createHint }}</p>
|
||||
<p>先创建扩展,再进入详情页继续补充版本、截图和发布信息。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ route('webadmin.packages.store') }}" class="grid">
|
||||
@csrf
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="field">
|
||||
<label>类型</label>
|
||||
<select class="select" name="type" required>
|
||||
<option value="plugin" @selected($currentType === 'plugin')>插件</option>
|
||||
<option value="theme" @selected($currentType === 'theme')>主题</option>
|
||||
<option value="plugin" @selected(old('type', $currentType ?: 'plugin') === 'plugin')>插件</option>
|
||||
<option value="theme" @selected(old('type', $currentType) === 'theme')>主题</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Slug</label>
|
||||
<input class="input" name="slug" placeholder="HelloStore" required>
|
||||
<input class="input" name="slug" value="{{ old('slug') }}" placeholder="HelloStore" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>名称</label>
|
||||
<input class="input" name="name" placeholder="Hello Store" required>
|
||||
<input class="input" name="name" value="{{ old('name') }}" placeholder="Hello Store" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>状态</label>
|
||||
<select class="select" name="status">
|
||||
<option value="published">published</option>
|
||||
<option value="draft">draft</option>
|
||||
<option value="hidden">hidden</option>
|
||||
<option value="deprecated">deprecated</option>
|
||||
@foreach ($statusLabels as $status => $label)
|
||||
<option value="{{ $status }}" @selected(old('status', 'published') === $status)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>作者</label>
|
||||
<input class="input" name="author" placeholder="LT083">
|
||||
<input class="input" name="author" value="{{ old('author') }}" placeholder="LT083">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>License</label>
|
||||
<input class="input" name="license" placeholder="MIT">
|
||||
<input class="input" name="license" value="{{ old('license') }}" placeholder="MIT">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>排序</label>
|
||||
<input class="input" type="number" name="sort_order" value="{{ old('sort_order', 0) }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>摘要</label>
|
||||
<input class="input" name="summary" placeholder="一句话说明这个{{ $entityLabel }}是做什么的">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>描述</label>
|
||||
<textarea name="description" placeholder="详细描述这个{{ $entityLabel }}的用途与定位"></textarea>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="field">
|
||||
<label>主页</label>
|
||||
<input class="input" name="homepage" placeholder="https://example.com/package">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>图标 URL</label>
|
||||
<input class="input" name="icon_url" placeholder="https://example.com/icon.png">
|
||||
<input class="input" name="homepage" value="{{ old('homepage') }}" placeholder="https://example.com/package">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>分类 slug,多个用空格分隔</label>
|
||||
<input class="input" name="categories_text" placeholder="seo performance">
|
||||
<label>图标 URL</label>
|
||||
<input class="input" name="icon_url" value="{{ old('icon_url') }}" placeholder="https://example.com/icon.png">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>摘要</label>
|
||||
<input class="input" name="summary" value="{{ old('summary') }}" placeholder="一句话说明这个{{ $entityLabel }}是做什么的">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>描述</label>
|
||||
<textarea name="description" placeholder="补充更完整的定位与功能说明">{{ old('description') }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>分类名称或 slug,优先用逗号分隔</label>
|
||||
<input class="input" name="categories_text" value="{{ old('categories_text') }}" placeholder="性能优化, seo">
|
||||
</div>
|
||||
|
||||
<div class="checkbox-row">
|
||||
<label><input type="checkbox" name="is_featured" value="1" @checked(old('is_featured'))> 设为推荐扩展</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn" type="submit">创建{{ $entityLabel }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@@ -1,82 +1,257 @@
|
||||
@php
|
||||
$typeLabel = $package->type === 'plugin' ? '插件' : '主题';
|
||||
$statusLabels = [
|
||||
'draft' => '草稿',
|
||||
'published' => '发布中',
|
||||
'hidden' => '隐藏',
|
||||
'deprecated' => '弃用',
|
||||
];
|
||||
$statusTone = [
|
||||
'draft' => 'muted',
|
||||
'published' => 'ok',
|
||||
'hidden' => 'warn',
|
||||
'deprecated' => 'warn',
|
||||
];
|
||||
$currentVersion = $package->versions->firstWhere('is_latest', true)
|
||||
?: $package->versions->firstWhere('is_stable', true)
|
||||
?: $package->versions->first();
|
||||
@endphp
|
||||
|
||||
@extends('admin.layout', [
|
||||
'title' => 'Tstore Admin · ' . $package->name,
|
||||
'pageTitle' => $package->name,
|
||||
'pageSubtitle' => ($package->type === 'plugin' ? '插件' : ($package->type === 'theme' ? '主题' : $package->type)) . ' · ' . $package->slug,
|
||||
'pageSubtitle' => $typeLabel . ' · ' . $package->slug,
|
||||
'pageBadge' => ucfirst($package->type),
|
||||
])
|
||||
|
||||
@php
|
||||
$entityLabel = $package->type === 'plugin' ? '插件' : ($package->type === 'theme' ? '主题' : '扩展');
|
||||
@endphp
|
||||
|
||||
@section('content')
|
||||
<div class="grid">
|
||||
<section class="stats">
|
||||
<div class="stat"><span class="label">扩展类型</span><span class="value">{{ strtoupper($package->type) }}</span><span class="hint">{{ $typeLabel }} package</span></div>
|
||||
<div class="stat"><span class="label">当前状态</span><span class="value">{{ $statusLabels[$package->status] ?? $package->status }}</span><span class="hint">可在下方直接调整</span></div>
|
||||
<div class="stat"><span class="label">当前主版本</span><span class="value">{{ $currentVersion?->version ?: '-' }}</span><span class="hint">最新或稳定版本</span></div>
|
||||
<div class="stat"><span class="label">版本数量</span><span class="value">{{ $package->versions->count() }}</span><span class="hint">历史记录总数</span></div>
|
||||
<div class="stat"><span class="label">截图数量</span><span class="value">{{ $package->screenshots->count() }}</span><span class="hint">当前展示素材</span></div>
|
||||
<div class="stat"><span class="label">下载次数</span><span class="value">{{ $package->download_count }}</span><span class="hint">累计下载总量</span></div>
|
||||
</section>
|
||||
|
||||
<div class="split">
|
||||
<div class="panel">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>{{ $entityLabel }}信息</h2>
|
||||
<p>编辑基础元数据、分类、图标与发布状态。</p>
|
||||
<section class="panel">
|
||||
<div class="entity-head">
|
||||
<div class="entity-icon">
|
||||
@if ($package->icon_url)
|
||||
<img src="{{ $package->icon_url }}" alt="{{ $package->name }}">
|
||||
@else
|
||||
{{ $package->type === 'plugin' ? 'P' : 'T' }}
|
||||
@endif
|
||||
</div>
|
||||
<div class="entity-copy">
|
||||
<h2>基础资料</h2>
|
||||
<p>这里维护扩展展示信息、排序、推荐状态以及分类关联。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ route('webadmin.packages.update', [$package->type, $package->slug]) }}" class="grid">
|
||||
@csrf @method('PUT')
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="field"><label>类型</label><input class="input" name="type" value="{{ $package->type }}" readonly></div>
|
||||
<div class="field"><label>Slug</label><input class="input" name="slug" value="{{ $package->slug }}" readonly></div>
|
||||
<div class="field"><label>名称</label><input class="input" name="name" value="{{ $package->name }}" required></div>
|
||||
<div class="field"><label>状态</label>
|
||||
<div class="field">
|
||||
<label>类型</label>
|
||||
<input class="input" name="type" value="{{ $package->type }}" readonly>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Slug</label>
|
||||
<input class="input" name="slug" value="{{ $package->slug }}" readonly>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>名称</label>
|
||||
<input class="input" name="name" value="{{ old('name', $package->name) }}" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>状态</label>
|
||||
<select class="select" name="status">
|
||||
@foreach (['draft','published','hidden','deprecated'] as $status)
|
||||
<option value="{{ $status }}" @selected($package->status === $status)>{{ $status }}</option>
|
||||
@foreach ($statusLabels as $status => $label)
|
||||
<option value="{{ $status }}" @selected(old('status', $package->status) === $status)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="field"><label>作者</label><input class="input" name="author" value="{{ $package->author }}"></div>
|
||||
<div class="field"><label>License</label><input class="input" name="license" value="{{ $package->license }}"></div>
|
||||
<div class="field"><label>主页</label><input class="input" name="homepage" value="{{ $package->homepage }}"></div>
|
||||
<div class="field"><label>图标 URL</label><input class="input" name="icon_url" value="{{ $package->icon_url }}"></div>
|
||||
<div class="field">
|
||||
<label>作者</label>
|
||||
<input class="input" name="author" value="{{ old('author', $package->author) }}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>License</label>
|
||||
<input class="input" name="license" value="{{ old('license', $package->license) }}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>排序</label>
|
||||
<input class="input" type="number" name="sort_order" value="{{ old('sort_order', $package->sort_order ?? 0) }}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>主页</label>
|
||||
<input class="input" name="homepage" value="{{ old('homepage', $package->homepage) }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field"><label>摘要</label><input class="input" name="summary" value="{{ $package->summary }}"></div>
|
||||
<div class="field"><label>描述</label><textarea name="description">{{ $package->description }}</textarea></div>
|
||||
<div class="field"><label>分类 slug,多个用逗号或空格分隔</label><input class="input" name="categories_text" value="{{ $package->categories->pluck('slug')->implode(', ') }}"></div>
|
||||
|
||||
<div class="field">
|
||||
<label>图标 URL</label>
|
||||
<input class="input" name="icon_url" value="{{ old('icon_url', $package->icon_url) }}">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>摘要</label>
|
||||
<input class="input" name="summary" value="{{ old('summary', $package->summary) }}">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>描述</label>
|
||||
<textarea name="description">{{ old('description', $package->description) }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>分类名称或 slug,优先用逗号分隔</label>
|
||||
<input class="input" name="categories_text" value="{{ old('categories_text', $package->categories->map(fn ($category) => $category->name ?: $category->slug)->implode(', ')) }}">
|
||||
</div>
|
||||
|
||||
<div class="checkbox-row">
|
||||
<label><input type="checkbox" name="is_featured" value="1" @checked(old('is_featured', $package->is_featured))> 设为推荐扩展</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn" type="submit">保存{{ $entityLabel }}信息</button>
|
||||
<a class="btn secondary" href="{{ route('webadmin.packages', ['type' => $package->type]) }}">返回{{ $entityLabel }}列表</a>
|
||||
<button class="btn" type="submit">保存{{ $typeLabel }}信息</button>
|
||||
<a class="btn ghost" href="{{ route('webadmin.packages', ['type' => $package->type]) }}">返回{{ $typeLabel }}列表</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>当前状态</h2>
|
||||
<p>快速查看分类、版本、下载与截图信息。</p>
|
||||
<div class="form-actions" style="margin-top:16px">
|
||||
<form method="post" action="{{ route('webadmin.packages.destroy', [$package->type, $package->slug]) }}" onsubmit="return confirm('确认删除这个{{ $typeLabel }}吗?版本、截图和分类关联都会一起删除。');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button class="btn danger" type="submit">删除{{ $typeLabel }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="stack">
|
||||
<section class="panel soft">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>当前资料</h2>
|
||||
<p>快速核对基础信息、时间戳和前台展示状态。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list">
|
||||
<div class="mini-card"><h3>状态</h3><div class="tags"><span class="chip {{ $package->status === 'published' ? 'ok' : 'warn' }}">{{ $package->status }}</span><span class="chip">下载 {{ $package->download_count }}</span><span class="chip {{ $package->is_featured ? 'ok' : 'muted' }}">{{ $package->is_featured ? '推荐' : '普通' }}</span></div></div>
|
||||
<div class="mini-card"><h3>分类</h3><div class="tags">@forelse($package->categories as $category)<span class="chip">{{ $category->name }}</span>@empty<span class="muted">尚未关联分类</span>@endforelse</div></div>
|
||||
<div class="mini-card"><h3>截图</h3><div class="muted">{{ $package->screenshots->count() }} 张</div></div>
|
||||
<div class="mini-card"><h3>最新版本</h3><div class="muted">{{ $package->latest_version ?: '尚未发布' }}</div></div>
|
||||
</div>
|
||||
|
||||
<div class="kv-grid">
|
||||
<div class="kv-card">
|
||||
<span class="kv-label">Slug</span>
|
||||
<span class="kv-value">{{ $package->slug }}</span>
|
||||
</div>
|
||||
<div class="kv-card">
|
||||
<span class="kv-label">状态</span>
|
||||
<span class="kv-value"><span class="chip {{ $statusTone[$package->status] ?? 'muted' }}">{{ $statusLabels[$package->status] ?? $package->status }}</span></span>
|
||||
</div>
|
||||
<div class="kv-card">
|
||||
<span class="kv-label">作者</span>
|
||||
<span class="kv-value">{{ $package->author ?: '未填写' }}</span>
|
||||
</div>
|
||||
<div class="kv-card">
|
||||
<span class="kv-label">主页</span>
|
||||
<span class="kv-value">{{ $package->homepage ?: '未填写' }}</span>
|
||||
</div>
|
||||
<div class="kv-card">
|
||||
<span class="kv-label">创建时间</span>
|
||||
<span class="kv-value">{{ optional($package->created_at)->format('Y-m-d H:i') ?: '-' }}</span>
|
||||
</div>
|
||||
<div class="kv-card">
|
||||
<span class="kv-label">更新时间</span>
|
||||
<span class="kv-value">{{ optional($package->updated_at)->format('Y-m-d H:i') ?: '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tags">
|
||||
@foreach ($package->categories as $category)
|
||||
<span class="chip">{{ $category->name }}</span>
|
||||
@endforeach
|
||||
@if ($package->categories->isEmpty())
|
||||
<span class="muted">尚未关联分类</span>
|
||||
@endif
|
||||
<span class="chip {{ $package->is_featured ? 'ok' : 'muted' }}">{{ $package->is_featured ? '推荐扩展' : '普通扩展' }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel soft">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>截图管理</h2>
|
||||
<p>手动录入详情页预览截图,第一张会优先作为前台主预览展示。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ route('webadmin.packages.screenshots.store', [$package->type, $package->slug]) }}" class="grid">
|
||||
@csrf
|
||||
|
||||
<div class="field">
|
||||
<label>图片 URL</label>
|
||||
<input class="input" name="image_url" value="{{ old('image_url') }}" placeholder="https://example.com/screenshots/home.webp" required>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>截图说明</label>
|
||||
<input class="input" name="caption" value="{{ old('caption') }}" placeholder="首页、设置页、文章页等">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>排序</label>
|
||||
<input class="input" type="number" name="sort_order" value="{{ old('sort_order', 0) }}">
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn" type="submit">添加截图</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@if ($package->screenshots->isNotEmpty())
|
||||
<div class="preview-grid" style="margin-top:18px">
|
||||
@foreach ($package->screenshots as $shot)
|
||||
<article class="preview-card">
|
||||
<figure class="preview-shot">
|
||||
<img src="{{ $shot->image_url }}" alt="{{ $shot->caption ?: $package->name }}">
|
||||
<figcaption>{{ $shot->caption ?: $package->name . ' 截图' }}</figcaption>
|
||||
</figure>
|
||||
<div class="preview-tools">
|
||||
<span class="chip muted">排序 {{ $shot->sort_order }}</span>
|
||||
<form method="post" action="{{ route('webadmin.packages.screenshots.destroy', [$package->type, $package->slug, $shot->id]) }}" onsubmit="return confirm('确认删除这张截图吗?');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button class="btn danger small" type="submit">删除</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="empty">当前还没有截图。</div>
|
||||
@endif
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="split">
|
||||
<div class="panel">
|
||||
<section class="panel">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>版本记录</h2>
|
||||
<p>查看版本状态,也可以直接删除历史版本。</p>
|
||||
<p>统一查看版本号、兼容区间、发布时间与稳定状态。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>版本</th>
|
||||
<th>稳定版</th>
|
||||
<th>最新</th>
|
||||
<th>状态</th>
|
||||
<th>兼容性</th>
|
||||
<th>发布时间</th>
|
||||
<th>操作</th>
|
||||
@@ -86,94 +261,173 @@
|
||||
@forelse ($package->versions as $version)
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ $version->version }}</strong>
|
||||
<div class="muted">{{ $version->package_url ?: '未设置 package_url' }}</div>
|
||||
<strong>v{{ $version->version }}</strong>
|
||||
<div class="muted">{{ $version->package_url ?: '未生成下载地址' }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="tags" style="margin-top:0">
|
||||
<span class="chip {{ $version->is_stable ? 'ok' : 'warn' }}">{{ $version->is_stable ? '稳定版' : '预发布' }}</span>
|
||||
<span class="chip {{ $version->is_latest ? 'ok' : 'muted' }}">{{ $version->is_latest ? '最新' : '历史' }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="chip {{ $version->is_stable ? 'ok' : 'warn' }}">{{ $version->is_stable ? '是' : '否' }}</span></td>
|
||||
<td><span class="chip {{ $version->is_latest ? 'ok' : 'muted' }}">{{ $version->is_latest ? '最新' : '历史' }}</span></td>
|
||||
<td>
|
||||
<div class="muted">Typecho {{ $version->typecho_min ?: '-' }} ~ {{ $version->typecho_max ?: '-' }}</div>
|
||||
<div class="muted">PHP {{ $version->php_min ?: '-' }} ~ {{ $version->php_max ?: '-' }}</div>
|
||||
</td>
|
||||
<td>{{ optional($version->published_at)->format('Y-m-d H:i') }}</td>
|
||||
<td>{{ optional($version->published_at)->format('Y-m-d H:i') ?: '-' }}</td>
|
||||
<td>
|
||||
<form method="post" action="{{ route('webadmin.packages.versions.destroy', [$package->type, $package->slug, $version->id]) }}" onsubmit="return confirm('确认删除这个版本吗?');">
|
||||
@csrf @method('DELETE')
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button class="btn danger small" type="submit">删除</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="6"><div class="empty">当前还没有版本记录。</div></td></tr>
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div class="empty">当前还没有版本记录。</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="panel">
|
||||
<section class="panel soft">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>zip 上传发布</h2>
|
||||
<p>优先读取 zip 根目录的 manifest.json;如果没有 manifest.json,则使用下方手动填写的版本和兼容性字段。</p>
|
||||
<h2>zip 发布</h2>
|
||||
<p>上传 zip 后补充版本字段,服务端会校验目录结构并生成可供 API 返回的版本元数据。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ route('webadmin.packages.publish', [$package->type, $package->slug]) }}" class="grid" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<div class="field"><label>zip 文件</label><input class="input" type="file" name="package_file" accept=".zip" required></div>
|
||||
<div class="field"><label>版本号(无 manifest 时必填)</label><input class="input" name="version" value="{{ old('version') }}" placeholder="1.0.0"></div>
|
||||
<div class="field"><label>发布说明(可选)</label><textarea name="changelog" placeholder="补充这次发布说明">{{ old('changelog') }}</textarea></div>
|
||||
<div class="form-grid">
|
||||
<div class="field"><label>Typecho Min</label><input class="input" name="typecho_min" value="{{ old('typecho_min', '1.2.0') }}" placeholder="1.2.0"></div>
|
||||
<div class="field"><label>Typecho Max</label><input class="input" name="typecho_max" value="{{ old('typecho_max') }}" placeholder="1.3.*"></div>
|
||||
<div class="field"><label>PHP Min</label><input class="input" name="php_min" value="{{ old('php_min', '7.4') }}" placeholder="7.4"></div>
|
||||
<div class="field"><label>PHP Max</label><input class="input" name="php_max" value="{{ old('php_max') }}" placeholder="8.3"></div>
|
||||
<div class="field"><label>PHP Extensions</label><input class="input" name="php_extensions" value="{{ is_array(old('php_extensions')) ? implode(',', old('php_extensions')) : old('php_extensions') }}" placeholder="curl,json"></div>
|
||||
<div class="field"><label>发布时间(可选)</label><input class="input" type="datetime-local" name="published_at" value="{{ old('published_at') }}"></div>
|
||||
|
||||
<div class="field">
|
||||
<label>zip 文件</label>
|
||||
<input class="input" type="file" name="package_file" accept=".zip" required>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="field"><label>发布选项</label>
|
||||
<div class="tags">
|
||||
<label><input type="checkbox" name="is_stable" value="1" @checked(old('is_stable', 1))> 设为稳定版</label>
|
||||
<label><input type="checkbox" name="mark_as_latest" value="1" @checked(old('mark_as_latest', 1))> 设为最新</label>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>版本号</label>
|
||||
<input class="input" name="version" value="{{ old('version') }}" placeholder="如 zip 不含 manifest,可在这里手动填写 1.0.0">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>发布说明</label>
|
||||
<textarea name="changelog" placeholder="补充这次发布的主要变化">{{ old('changelog') }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-grid compact">
|
||||
<div class="field">
|
||||
<label>Typecho Min</label>
|
||||
<input class="input" name="typecho_min" value="{{ old('typecho_min', '1.2.0') }}" placeholder="1.2.0">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Typecho Max</label>
|
||||
<input class="input" name="typecho_max" value="{{ old('typecho_max') }}" placeholder="1.3.*">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>PHP Min</label>
|
||||
<input class="input" name="php_min" value="{{ old('php_min', '7.4') }}" placeholder="7.4">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>PHP Max</label>
|
||||
<input class="input" name="php_max" value="{{ old('php_max') }}" placeholder="8.3">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>PHP Extensions</label>
|
||||
<input class="input" name="php_extensions" value="{{ is_array(old('php_extensions')) ? implode(',', old('php_extensions')) : old('php_extensions') }}" placeholder="curl,json">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>发布时间</label>
|
||||
<input class="input" type="datetime-local" name="published_at" value="{{ old('published_at') }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions"><button class="btn" type="submit">上传并发布</button></div>
|
||||
|
||||
<div class="checkbox-row">
|
||||
<label><input type="checkbox" name="is_stable" value="1" @checked(old('is_stable', 1))> 设为稳定版</label>
|
||||
<label><input type="checkbox" name="mark_as_latest" value="1" @checked(old('mark_as_latest', 1))> 设为最新版本</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn" type="submit">上传并发布</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<section class="panel">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>手动添加版本</h2>
|
||||
<p>调试阶段可以快速手动录入版本元数据。</p>
|
||||
<p>调试或补录历史版本时,可以直接写入元数据而不经过 zip 发布流程。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ route('webadmin.packages.versions.store', [$package->type, $package->slug]) }}" class="grid">
|
||||
@csrf
|
||||
<div class="form-grid">
|
||||
<div class="field"><label>版本号</label><input class="input" name="version" placeholder="1.0.0" required></div>
|
||||
<div class="field"><label>发布时间</label><input class="input" type="datetime-local" name="published_at"></div>
|
||||
<div class="field"><label>Typecho Min</label><input class="input" name="typecho_min" placeholder="1.2.0"></div>
|
||||
<div class="field"><label>Typecho Max</label><input class="input" name="typecho_max" placeholder="1.3.*"></div>
|
||||
<div class="field"><label>PHP Min</label><input class="input" name="php_min" placeholder="7.4"></div>
|
||||
<div class="field"><label>PHP Max</label><input class="input" name="php_max" placeholder="8.3"></div>
|
||||
<div class="field"><label>Package URL</label><input class="input" name="package_url" placeholder="https://..."></div>
|
||||
<div class="field"><label>Size</label><input class="input" name="package_size" type="number" placeholder="238000"></div>
|
||||
<div class="field"><label>SHA256</label><input class="input" name="sha256" placeholder="可选,默认会补 64 位 0"></div>
|
||||
<div class="field"><label>PHP Extensions</label><input class="input" name="php_extensions" placeholder="curl,json"></div>
|
||||
|
||||
<div class="form-grid compact">
|
||||
<div class="field">
|
||||
<label>版本号</label>
|
||||
<input class="input" name="version" value="{{ old('version') }}" placeholder="1.0.0" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>发布时间</label>
|
||||
<input class="input" type="datetime-local" name="published_at" value="{{ old('published_at') }}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Typecho Min</label>
|
||||
<input class="input" name="typecho_min" value="{{ old('typecho_min') }}" placeholder="1.2.0">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Typecho Max</label>
|
||||
<input class="input" name="typecho_max" value="{{ old('typecho_max') }}" placeholder="1.3.*">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>PHP Min</label>
|
||||
<input class="input" name="php_min" value="{{ old('php_min') }}" placeholder="7.4">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>PHP Max</label>
|
||||
<input class="input" name="php_max" value="{{ old('php_max') }}" placeholder="8.3">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Package URL</label>
|
||||
<input class="input" name="package_url" value="{{ old('package_url') }}" placeholder="https://...">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Size</label>
|
||||
<input class="input" type="number" name="package_size" value="{{ old('package_size') }}" placeholder="238000">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>SHA256</label>
|
||||
<input class="input" name="sha256" value="{{ old('sha256') }}" placeholder="可选,默认会补 64 位 0">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>PHP Extensions</label>
|
||||
<input class="input" name="php_extensions" value="{{ old('php_extensions') }}" placeholder="curl,json">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field"><label>发布说明</label><textarea name="changelog" placeholder="输入更新内容"></textarea></div>
|
||||
<div class="form-grid">
|
||||
<label><input type="checkbox" name="is_stable" value="1"> 设为稳定版</label>
|
||||
<label><input type="checkbox" name="is_latest" value="1"> 设为最新版本</label>
|
||||
|
||||
<div class="field">
|
||||
<label>发布说明</label>
|
||||
<textarea name="changelog" placeholder="输入更新内容">{{ old('changelog') }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="checkbox-row">
|
||||
<label><input type="checkbox" name="is_stable" value="1" @checked(old('is_stable'))> 设为稳定版</label>
|
||||
<label><input type="checkbox" name="is_latest" value="1" @checked(old('is_latest'))> 设为最新版本</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn" type="submit">添加版本</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
@extends('admin.layout', [
|
||||
'title' => 'Tstore Admin · 站点设置',
|
||||
'pageTitle' => '站点设置',
|
||||
'pageSubtitle' => '配置前台站点名称、标语,以及首页主要展示文案。',
|
||||
'pageBadge' => 'Settings',
|
||||
])
|
||||
|
||||
@section('content')
|
||||
<div class="grid">
|
||||
<section class="panel soft">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>站点基础信息</h2>
|
||||
<p>这些字段会影响前台标题、品牌名称和首页头部展示。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ route('webadmin.settings.update') }}" class="grid">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="field">
|
||||
<label>站点名称</label>
|
||||
<input class="input" name="site_name" value="{{ old('site_name', $settings['site_name']) }}" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>站点标语</label>
|
||||
<input class="input" name="site_tagline" value="{{ old('site_tagline', $settings['site_tagline']) }}" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>首页标题副标题</label>
|
||||
<input class="input" name="home_title_suffix" value="{{ old('home_title_suffix', $settings['home_title_suffix']) }}" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>首页眉标</label>
|
||||
<input class="input" name="home_eyebrow" value="{{ old('home_eyebrow', $settings['home_eyebrow']) }}" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field" style="max-width:420px">
|
||||
<label>首页侧栏眉标</label>
|
||||
<input class="input" name="home_aside_kicker" value="{{ old('home_aside_kicker', $settings['home_aside_kicker']) }}" required>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>首页主标题</label>
|
||||
<input class="input" name="home_headline" value="{{ old('home_headline', $settings['home_headline']) }}" required>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>首页主说明</label>
|
||||
<textarea name="home_lede" required>{{ old('home_lede', $settings['home_lede']) }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>首页侧栏标题</label>
|
||||
<input class="input" name="home_aside_title" value="{{ old('home_aside_title', $settings['home_aside_title']) }}" required>
|
||||
</div>
|
||||
|
||||
<div class="split">
|
||||
<section class="panel soft">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>首页说明卡片</h2>
|
||||
<p>右侧三块说明文字都可以在这里调整。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="field">
|
||||
<label>卡片一标题</label>
|
||||
<input class="input" name="home_feature_one_title" value="{{ old('home_feature_one_title', $settings['home_feature_one_title']) }}" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>卡片一说明</label>
|
||||
<textarea name="home_feature_one_body" required>{{ old('home_feature_one_body', $settings['home_feature_one_body']) }}</textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>卡片二标题</label>
|
||||
<input class="input" name="home_feature_two_title" value="{{ old('home_feature_two_title', $settings['home_feature_two_title']) }}" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>卡片二说明</label>
|
||||
<textarea name="home_feature_two_body" required>{{ old('home_feature_two_body', $settings['home_feature_two_body']) }}</textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>卡片三标题</label>
|
||||
<input class="input" name="home_feature_three_title" value="{{ old('home_feature_three_title', $settings['home_feature_three_title']) }}" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>卡片三说明</label>
|
||||
<textarea name="home_feature_three_body" required>{{ old('home_feature_three_body', $settings['home_feature_three_body']) }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel soft">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>列表区域标题</h2>
|
||||
<p>首页的推荐插件和推荐主题标题、说明都能单独设置。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="field">
|
||||
<label>插件区标题</label>
|
||||
<input class="input" name="home_plugins_title" value="{{ old('home_plugins_title', $settings['home_plugins_title']) }}" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>插件区说明</label>
|
||||
<textarea name="home_plugins_subtitle" required>{{ old('home_plugins_subtitle', $settings['home_plugins_subtitle']) }}</textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>主题区标题</label>
|
||||
<input class="input" name="home_themes_title" value="{{ old('home_themes_title', $settings['home_themes_title']) }}" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>主题区说明</label>
|
||||
<textarea name="home_themes_subtitle" required>{{ old('home_themes_subtitle', $settings['home_themes_subtitle']) }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn" type="submit">保存站点设置</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
@endsection
|
||||
Reference in New Issue
Block a user