前端极简黑白重设计,并纳入客户端注册、站点设置与后台相关改动

- storefront 全站改为极简黑白配色:直角、细线分隔、卡片 hover 浅底、深色模式(跟随系统+手动切换)
- 抽出 storefront/partials/card 复用卡片
- 一并提交此前暂存的客户端注册 API、站点设置、后台页面与测试

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
浪子
2026-09-22 08:35:49 +08:00
parent 62cffc6f5f
commit 04a2034975
41 changed files with 3447 additions and 658 deletions
+90
View File
@@ -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
+130 -77
View File
@@ -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
+107 -32
View File
@@ -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>
+297 -62
View File
@@ -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>
+13 -10
View File
@@ -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>
+113 -77
View File
@@ -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
+344 -90
View File
@@ -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
+133
View File
@@ -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
+50 -89
View File
@@ -1,102 +1,63 @@
@extends('storefront.layout', ['title' => 'Tstore · 扩展展示站'])
@extends('storefront.layout', ['title' => (config('app.name', 'Tstore')) . ' · ' . ($storeSettings['home_title_suffix'] ?? '扩展展示站')])
@section('hero')
<div class="hero-grid">
<div class="hero-card">
<h1>精选 Typecho 插件与主题展示</h1>
<p>这里是展示前台,只负责展示站点里的插件、主题、分类、截图和版本信息,不直接暴露下载地址。</p>
<div class="hero-meta">
<div class="hero-stat"><strong>{{ $stats['packages'] }}</strong><span>已发布扩展</span></div>
<div class="hero-stat"><strong>{{ $stats['plugins'] }}</strong><span>插件</span></div>
<div class="hero-stat"><strong>{{ $stats['themes'] }}</strong><span>主题</span></div>
<div class="hero-stat"><strong>{{ $stats['categories'] }}</strong><span>分类</span></div>
<section class="hero">
<div class="container">
<span class="eyebrow">{{ $storeSettings['home_eyebrow'] ?? 'Public Showcase' }}</span>
<h1 class="headline">{{ $storeSettings['home_headline'] ?? '更干净的 Typecho 扩展展示站' }}</h1>
<p class="lede">{{ $storeSettings['home_lede'] ?? '前台只负责展示插件与主题的资料、分类、兼容性和截图,真正的发布、上传和站点接入都由服务端统一维护,信息层次更清楚。' }}</p>
<div class="hero-actions">
<a class="btn" href="{{ route('storefront.plugins') }}">浏览插件</a>
<a class="btn ghost" href="{{ route('storefront.themes') }}">浏览主题</a>
</div>
<div class="stats">
<div class="stat"><strong>{{ $stats['packages'] }}</strong><span>已发布扩展</span></div>
<div class="stat"><strong>{{ $stats['plugins'] }}</strong><span>插件</span></div>
<div class="stat"><strong>{{ $stats['themes'] }}</strong><span>主题</span></div>
<div class="stat"><strong>{{ $stats['categories'] }}</strong><span>分类</span></div>
</div>
</div>
<div class="hero-card">
<h3 style="margin:0 0 10px;font-size:20px">当前站点定位</h3>
<p>前台面向访客展示内容,后台则面向管理员,通过账号密码登录后维护扩展、分类、版本和 zip 发布。</p>
<div class="card-actions" style="margin-top:18px">
<a class="btn" href="{{ route('storefront.plugins') }}">查看插件</a>
<a class="btn secondary" href="{{ route('storefront.themes') }}">查看主题</a>
</div>
</div>
</div>
</section>
@endsection
@section('content')
<div class="panel">
<div class="section-head">
<div>
<h2>推荐插件</h2>
<p>展示基础信息、版本兼容性与分类,不直接提供 zip 下载地址。</p>
</div>
<a class="btn secondary" href="{{ route('storefront.plugins') }}">查看全部插件</a>
</div>
<div class="grid">
@forelse($featuredPlugins as $package)
<div class="card">
<div class="card-top">
@if($package->icon_url)
<img class="icon" src="{{ $package->icon_url }}" alt="{{ $package->name }}">
@else
<div class="icon"></div>
@endif
<div>
<div class="title">{{ $package->name }}</div>
<div class="muted">{{ $package->author ?: '未知作者' }} · {{ $package->latestStableVersion?->version ?: $package->latest_version }}</div>
</div>
</div>
<div class="summary">{{ $package->summary ?: '暂无摘要' }}</div>
<div class="tags">
@foreach($package->categories as $category)
<span class="chip">{{ $category->name }}</span>
@endforeach
</div>
<div class="card-actions">
<a class="btn secondary" href="{{ route('storefront.show', [$package->type, $package->slug]) }}">查看详情</a>
</div>
<section class="section">
<div class="container">
<div class="section-head">
<div>
<span class="section-kicker">Plugins</span>
<h2>{{ $storeSettings['home_plugins_title'] ?? '推荐插件' }}</h2>
<p>{{ $storeSettings['home_plugins_subtitle'] ?? '优先展示适合作为目录首页入口的插件,突出摘要、作者、分类和版本信息。' }}</p>
</div>
@empty
<div class="empty">当前没有推荐插件。</div>
@endforelse
<a class="btn ghost sm" href="{{ route('storefront.plugins') }}">查看全部</a>
</div>
<div class="grid">
@forelse($featuredPlugins as $package)
@include('storefront.partials.card', ['package' => $package, 'typeLabel' => '插件', 'fallback' => 'P'])
@empty
<div class="empty">当前还没有推荐插件。</div>
@endforelse
</div>
</div>
</div>
</section>
<div class="panel section">
<div class="section-head">
<div>
<h2>推荐主题</h2>
<p>适合做展示站首页,突出视觉风格、分类和版本信息。</p>
</div>
<a class="btn secondary" href="{{ route('storefront.themes') }}">查看全部主题</a>
</div>
<div class="grid">
@forelse($featuredThemes as $package)
<div class="card">
<div class="card-top">
@if($package->icon_url)
<img class="icon" src="{{ $package->icon_url }}" alt="{{ $package->name }}">
@else
<div class="icon"></div>
@endif
<div>
<div class="title">{{ $package->name }}</div>
<div class="muted">{{ $package->author ?: '未知作者' }} · {{ $package->latestStableVersion?->version ?: $package->latest_version }}</div>
</div>
</div>
<div class="summary">{{ $package->summary ?: '暂无摘要' }}</div>
<div class="tags">
@foreach($package->categories as $category)
<span class="chip">{{ $category->name }}</span>
@endforeach
</div>
<div class="card-actions">
<a class="btn secondary" href="{{ route('storefront.show', [$package->type, $package->slug]) }}">查看详情</a>
</div>
<section class="section">
<div class="container">
<div class="section-head">
<div>
<span class="section-kicker">Themes</span>
<h2>{{ $storeSettings['home_themes_title'] ?? '推荐主题' }}</h2>
<p>{{ $storeSettings['home_themes_subtitle'] ?? '主题列表保持同样的展示逻辑,方便在统一视觉下浏览不同类型的扩展内容。' }}</p>
</div>
@empty
<div class="empty">当前没有推荐主题。</div>
@endforelse
<a class="btn ghost sm" href="{{ route('storefront.themes') }}">查看全部</a>
</div>
<div class="grid">
@forelse($featuredThemes as $package)
@include('storefront.partials.card', ['package' => $package, 'typeLabel' => '主题', 'fallback' => 'T'])
@empty
<div class="empty">当前还没有推荐主题。</div>
@endforelse
</div>
</div>
</div>
</section>
@endsection
+182 -30
View File
@@ -3,42 +3,194 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ $title ?? 'Tstore' }}</title>
<title>{{ $title ?? ($storeSettings['site_name'] ?? config('app.name', 'Tstore')) }}</title>
<script>
(function(){try{var t=localStorage.getItem('tstore-theme');if(t){document.documentElement.setAttribute('data-theme',t);}}catch(e){}})();
</script>
<style>
:root{--bg:#0f172a;--panel:#ffffff;--soft:#f5f8fc;--line:#dbe5f0;--text:#16324f;--muted:#607489;--brand:#2f6ea6;--brand-2:#61a6e3;--shadow:0 18px 48px rgba(15,23,42,.08)}
*{box-sizing:border-box}body{margin:0;font:14px/1.7 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;background:#f4f8fb;color:var(--text)}a{text-decoration:none;color:inherit}
.container{max-width:1180px;margin:0 auto;padding:0 20px}.hero{background:linear-gradient(135deg,#10233a 0%,#16324f 42%,#24527c 100%);color:#fff;padding:24px 0 84px}.nav{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:18px 0}.brand{font-size:22px;font-weight:800}.nav-links{display:flex;gap:10px;flex-wrap:wrap}.nav-links a{padding:10px 14px;border-radius:999px;color:rgba(240,247,255,.88)}.nav-links a:hover,.nav-links a.active{background:rgba(255,255,255,.12)}
.hero-grid{display:grid;grid-template-columns:1.15fr .85fr;gap:18px;align-items:center;padding-top:10px}.hero-card{background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.12);border-radius:26px;padding:26px;backdrop-filter:blur(10px)}.hero h1{margin:0 0 12px;font-size:44px;line-height:1.08;letter-spacing:-.03em}.hero p{margin:0;color:rgba(230,239,249,.9);max-width:680px}.hero-meta{display:flex;gap:12px;flex-wrap:wrap;margin-top:20px}.hero-stat{min-width:120px;padding:14px 16px;border-radius:18px;background:rgba(255,255,255,.12)}.hero-stat strong{display:block;font-size:24px}.hero-stat span{display:block;font-size:12px;color:rgba(231,239,248,.78);margin-top:4px}
.shell{margin-top:-52px;padding-bottom:48px}.panel{background:#fff;border:1px solid var(--line);border-radius:24px;padding:22px;box-shadow:var(--shadow)}.section{margin-top:18px}.section-head{display:flex;justify-content:space-between;align-items:end;gap:16px;margin-bottom:14px}.section-head h2{margin:0;font-size:24px}.section-head p{margin:6px 0 0;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{padding:11px 13px;border:1px solid #cfdae8;border-radius:12px;background:#f9fbfd;min-width:180px}.input:focus,.select:focus{outline:none;border-color:#77aee6;box-shadow:0 0 0 4px rgba(89,156,226,.12);background:#fff}
.btn{display:inline-flex;align-items:center;justify-content:center;padding:11px 15px;border-radius:12px;background:linear-gradient(135deg,var(--brand),var(--brand-2));color:#fff;font-weight:700;border:none;cursor:pointer}.btn.secondary{background:#fff;color:var(--brand);border:1px solid #cfe0f1}
.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:18px}.card{background:linear-gradient(180deg,#fff 0,#fbfdff 100%);border:1px solid var(--line);border-radius:22px;padding:18px;box-shadow:0 12px 28px rgba(15,23,42,.05)}.card-top{display:flex;gap:14px;align-items:flex-start}.icon{width:64px;height:64px;border-radius:18px;background:#edf4fb;object-fit:cover;border:1px solid #d9e6f3}.title{font-size:18px;font-weight:800;margin:0}.muted{color:var(--muted)}.summary{margin:14px 0;color:#4a6176;min-height:72px}.tags{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}.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}.card-actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:16px}
.stats-row{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px}.stat{padding:18px;border-radius:22px;background:linear-gradient(180deg,#ffffff 0,#f8fbfe 100%);border:1px solid var(--line)}.stat strong{display:block;font-size:30px;line-height:1.1}.stat span{display:block;margin-top:6px;color:var(--muted)}
.detail-grid{display:grid;grid-template-columns:1.2fr .8fr;gap:18px}.detail-card{background:#fff;border:1px solid var(--line);border-radius:22px;padding:20px;box-shadow:var(--shadow)}.detail-card h3{margin:0 0 12px;font-size:20px}.version-list{display:grid;gap:10px}.version{border:1px solid var(--line);border-radius:16px;padding:14px;background:#fbfdff}.version strong{font-size:16px}
.gallery{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.shot{border:1px solid var(--line);border-radius:16px;overflow:hidden;background:#fff}.shot img{width:100%;height:180px;object-fit:cover;display:block}.shot div{padding:10px 12px;color:var(--muted);font-size:13px}
.empty{padding:36px 18px;border:1px dashed var(--line);border-radius:18px;text-align:center;background:#fbfdff;color:var(--muted)}
@media (max-width:960px){.hero-grid,.detail-grid{grid-template-columns:1fr}.grid,.stats-row{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (max-width:640px){.grid,.stats-row,.gallery{grid-template-columns:1fr}.hero h1{font-size:34px}.shell{margin-top:-36px}.nav{flex-direction:column;align-items:flex-start}}
:root{
--bg:#ffffff;--ink:#111111;--ink-soft:#6b6b6b;--ink-mute:#9a9a9a;
--line:#e7e7e7;--line-strong:#111111;--hover:#f5f5f5;--panel:#ffffff;
}
:root[data-theme="dark"]{
--bg:#0a0a0a;--ink:#f2f2f2;--ink-soft:#a3a3a3;--ink-mute:#6f6f6f;
--line:#262626;--line-strong:#f2f2f2;--hover:#171717;--panel:#0f0f0f;
}
@media (prefers-color-scheme:dark){
:root:not([data-theme="light"]){
--bg:#0a0a0a;--ink:#f2f2f2;--ink-soft:#a3a3a3;--ink-mute:#6f6f6f;
--line:#262626;--line-strong:#f2f2f2;--hover:#171717;--panel:#0f0f0f;
}
}
*{box-sizing:border-box}
body{margin:0;font:15px/1.7 "Inter","PingFang SC","Microsoft YaHei UI",-apple-system,sans-serif;color:var(--ink);background:var(--bg);-webkit-font-smoothing:antialiased;transition:background .2s,color .2s}
a{color:inherit;text-decoration:none}
img{display:block;max-width:100%}
code{padding:2px 6px;background:var(--hover);border:1px solid var(--line);font-size:.9em}
.container{width:min(1080px,calc(100% - 48px));margin:0 auto}
/* 顶栏 */
.topbar{border-bottom:1px solid var(--line)}
.topbar-inner{display:flex;justify-content:space-between;align-items:center;height:64px;gap:12px}
.brand{display:flex;align-items:center;gap:10px;font-weight:600;font-size:16px}
.brand-mark{display:grid;place-items:center;width:24px;height:24px;background:var(--ink);color:var(--bg);font-size:11px;font-weight:700;letter-spacing:.02em}
.brand-copy strong{display:block;font-size:16px;line-height:1.1;font-weight:600}
.nav{display:flex;align-items:center;gap:4px;flex-wrap:wrap}
.nav a{padding:7px 12px;color:var(--ink-soft);font-size:14px;font-weight:500;transition:background .15s,color .15s}
.nav a:hover{background:var(--hover);color:var(--ink)}
.nav a.active{color:var(--ink);background:var(--hover)}
.theme-toggle{display:grid;place-items:center;width:34px;height:34px;margin-left:6px;border:1px solid var(--line);background:transparent;color:var(--ink-soft);cursor:pointer;font-size:15px;line-height:1;transition:border-color .15s,color .15s}
.theme-toggle:hover{border-color:var(--ink);color:var(--ink)}
/* hero */
.hero{padding:80px 0 52px;border-bottom:1px solid var(--line)}
.eyebrow{display:inline-block;font-size:12px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-mute)}
.headline{margin:18px 0 0;font-size:40px;line-height:1.12;letter-spacing:-.025em;font-weight:700}
.lede{margin:18px 0 0;max-width:44em;color:var(--ink-soft);font-size:16px;line-height:1.8}
.hero-actions{display:flex;gap:10px;margin-top:32px;flex-wrap:wrap}
/* 按钮 */
.btn{display:inline-flex;align-items:center;justify-content:center;height:40px;padding:0 18px;font-size:14px;font-weight:600;border:1px solid var(--line-strong);background:var(--ink);color:var(--bg);cursor:pointer;transition:opacity .15s,border-color .15s,background .15s}
.btn:hover{opacity:.82}
.btn.ghost{background:var(--panel);color:var(--ink);border-color:var(--line)}
.btn.ghost:hover{border-color:var(--ink);opacity:1}
.btn.sm{height:34px;padding:0 14px;font-size:13px}
/* 统计横排 */
.stats{display:flex;gap:52px;margin-top:44px;flex-wrap:wrap}
.stat strong{display:block;font-size:26px;font-weight:700;letter-spacing:-.015em}
.stat span{display:block;margin-top:3px;font-size:13px;color:var(--ink-mute)}
/* 区块 */
.section{padding:56px 0;border-bottom:1px solid var(--line)}
.section:last-of-type{border-bottom:0}
.section-head{display:flex;justify-content:space-between;align-items:flex-end;gap:16px;margin-bottom:32px}
.section-head h2{margin:0;font-size:22px;font-weight:700;letter-spacing:-.015em}
.section-head p{margin:7px 0 0;color:var(--ink-soft);font-size:14px;max-width:46em}
.section-kicker{display:block;font-size:12px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-mute);margin-bottom:10px}
/* 卡片网格 */
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px}
.card{display:flex;flex-direction:column;padding:22px;border:1px solid var(--line);background:var(--panel);transition:border-color .15s,background .15s}
.card:hover{border-color:var(--ink);background:var(--hover)}
.card-top{display:flex;gap:12px;align-items:center}
.card-icon{width:44px;height:44px;border:1px solid var(--line);object-fit:cover;flex:none;background:var(--hover)}
.card-placeholder{display:grid;place-items:center;font-weight:700;color:var(--ink-soft)}
.card-title{margin:0;font-size:16px;font-weight:600}
.card-meta{margin:3px 0 0;font-size:13px;color:var(--ink-mute)}
.card-summary{margin:18px 0 0;color:var(--ink-soft);font-size:14px;line-height:1.75;flex:1}
.tags{display:flex;gap:6px;flex-wrap:wrap;margin-top:18px}
.tag{font-size:12px;font-weight:500;padding:3px 9px;border:1px solid var(--line);color:var(--ink-soft)}
.tag.solid{background:var(--ink);color:var(--bg);border-color:var(--ink)}
.card-foot{display:flex;justify-content:space-between;align-items:center;margin-top:20px;padding-top:18px;border-top:1px solid var(--line)}
.card-foot .dl{font-size:13px;color:var(--ink-mute)}
/* 筛选表单 */
.filter-bar{display:flex;gap:14px;align-items:flex-end;flex-wrap:wrap;margin-top:28px}
.field{display:grid;gap:6px}
.field label{font-size:12px;font-weight:600;color:var(--ink-mute);letter-spacing:.06em;text-transform:uppercase}
.input,.select{height:40px;min-width:180px;width:100%;padding:0 12px;border:1px solid var(--line);background:var(--panel);color:var(--ink);font-size:14px;outline:none;transition:border-color .15s}
.input:focus,.select:focus{border-color:var(--ink)}
.input::placeholder{color:var(--ink-mute)}
/* 详情页 */
.detail-head{display:flex;gap:18px;align-items:flex-start}
.detail-icon{width:72px;height:72px;object-fit:cover;border:1px solid var(--line);background:var(--hover);flex:none}
.detail-placeholder{display:grid;place-items:center;font-size:26px;font-weight:700;color:var(--ink-soft)}
.detail-layout{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(300px,1fr);gap:32px}
.stack{display:grid;gap:28px}
.info-card + .info-card{padding-top:28px;border-top:1px solid var(--line)}
.info-card h3{margin:0 0 18px;font-size:18px;font-weight:700}
.rich-text{color:var(--ink-soft);line-height:1.9;white-space:pre-wrap}
.info-list{display:grid;gap:16px}
.info-item strong{display:block;font-size:12px;color:var(--ink-mute);letter-spacing:.06em;text-transform:uppercase}
.info-item span,.info-item a{display:block;margin-top:4px;color:var(--ink);word-break:break-word}
/* 版本记录 */
.version-list{display:grid;gap:0}
.version-item{padding:18px 0;border-top:1px solid var(--line)}
.version-item:first-child{border-top:0;padding-top:0}
.version-head{display:flex;justify-content:space-between;gap:12px;align-items:flex-start}
.version-title{font-size:15px;font-weight:700}
.version-meta{margin-top:4px;color:var(--ink-mute);font-size:13px}
/* 截图 */
.shots{display:grid;gap:16px}
.shot-feature,.shot-card{margin:0;border:1px solid var(--line);overflow:hidden;background:var(--panel)}
.shot-feature img{width:100%;height:360px;object-fit:cover;border-bottom:1px solid var(--line)}
.shot-feature figcaption{display:grid;gap:5px;padding:14px 16px}
.shot-feature strong{font-size:15px;font-weight:600}
.shot-feature span{color:var(--ink-mute);font-size:13px;line-height:1.6}
.shot-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:14px}
.shot-card img{width:100%;height:180px;object-fit:cover;border-bottom:1px solid var(--line)}
.shot-card figcaption{padding:10px 14px;color:var(--ink-mute);font-size:13px}
/* 空状态 */
.empty{padding:44px 22px;border:1px dashed var(--line);color:var(--ink-mute);text-align:center;font-size:14px}
/* footer */
.footer{padding:36px 0}
.footer-inner{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;color:var(--ink-mute);font-size:13px}
.footer-inner strong{color:var(--ink);font-weight:600}
/* 分页 */
nav[role="navigation"]{margin-top:36px}
nav[role="navigation"] > div:first-child{display:none}
nav[role="navigation"] > div:last-child{display:flex;justify-content:center;flex-wrap:wrap;gap:6px}
nav[role="navigation"] a,nav[role="navigation"] span{display:inline-flex;align-items:center;justify-content:center;min-width:38px;height:38px;padding:0 12px;border:1px solid var(--line);background:var(--panel);color:var(--ink);font-size:14px}
nav[role="navigation"] a:hover{border-color:var(--ink)}
nav[role="navigation"] span[aria-current="page"]{background:var(--ink);border-color:var(--ink);color:var(--bg)}
nav[role="navigation"] svg{display:none}
@media (max-width:820px){.detail-layout{grid-template-columns:1fr}}
@media (max-width:640px){
.container{width:min(100% - 32px,1080px)}
.hero{padding:56px 0 44px}
.headline{font-size:31px}
.stats{gap:32px;margin-top:36px}
.section{padding:44px 0}
.section-head{flex-direction:column;align-items:flex-start}
.shot-feature img{height:240px}
}
</style>
</head>
<body>
<header class="hero">
<div class="container">
<div class="nav">
<div class="brand"><a href="{{ route('storefront.home') }}">Tstore</a></div>
<div class="nav-links">
<a href="{{ route('storefront.home') }}" class="{{ request()->routeIs('storefront.home') ? 'active' : '' }}">首页</a>
<a href="{{ route('storefront.plugins') }}" class="{{ request()->routeIs('storefront.plugins') ? 'active' : '' }}">插件</a>
<a href="{{ route('storefront.themes') }}" class="{{ request()->routeIs('storefront.themes') ? 'active' : '' }}">主题</a>
<a href="{{ route('webadmin.login') }}">后台登录</a>
</div>
</div>
@yield('hero')
<header class="topbar">
<div class="container topbar-inner">
<a class="brand" href="{{ route('storefront.home') }}">
<span class="brand-mark">{{ mb_substr($storeSettings['site_name'] ?? 'T', 0, 1) }}</span>
<span class="brand-copy"><strong>{{ $storeSettings['site_name'] ?? config('app.name', 'Tstore') }}</strong></span>
</a>
<nav class="nav">
<a href="{{ route('storefront.home') }}" class="{{ request()->routeIs('storefront.home') ? 'active' : '' }}">首页</a>
<a href="{{ route('storefront.plugins') }}" class="{{ request()->routeIs('storefront.plugins') ? 'active' : '' }}">插件</a>
<a href="{{ route('storefront.themes') }}" class="{{ request()->routeIs('storefront.themes') ? 'active' : '' }}">主题</a>
<button type="button" class="theme-toggle" id="themeToggle" aria-label="切换深浅色">◐</button>
</nav>
</div>
</header>
<main class="shell">
<div class="container">
@yield('content')
</div>
@yield('hero')
<main>
@yield('content')
</main>
<footer class="footer">
<div class="container footer-inner">
<div><strong>{{ $storeSettings['site_name'] ?? config('app.name', 'Tstore') }}</strong> · {{ $storeSettings['site_tagline'] ?? 'Typecho Extension Directory' }}</div>
<div>Copyright © {{ date('Y') }} {{ $storeSettings['site_name'] ?? config('app.name', 'Tstore') }}. 版权所有。</div>
</div>
</footer>
<script>
(function(){
var btn=document.getElementById('themeToggle');
if(!btn)return;
var mql=window.matchMedia('(prefers-color-scheme:dark)');
function current(){var s=document.documentElement.getAttribute('data-theme');return s?s:(mql.matches?'dark':'light');}
btn.addEventListener('click',function(){
var next=current()==='dark'?'light':'dark';
document.documentElement.setAttribute('data-theme',next);
try{localStorage.setItem('tstore-theme',next);}catch(e){}
});
})();
</script>
</body>
</html>
+58 -67
View File
@@ -1,79 +1,70 @@
@extends('storefront.layout', ['title' => 'Tstore · ' . ($type === 'plugin' ? '插件' : '主题')])
@extends('storefront.layout', ['title' => (config('app.name', 'Tstore')) . ' · ' . ($type === 'plugin' ? '插件目录' : '主题目录')])
@php
$typeLabel = $type === 'plugin' ? '插件' : '主题';
$fallback = $type === 'plugin' ? 'P' : 'T';
$selectedCategory = '';
foreach ($categories as $category) {
if ($filters['category'] === $category->slug) {
$selectedCategory = $category->name;
break;
}
}
$sortLabel = $filters['sort'] === 'name' ? '名称排序' : '最近更新';
@endphp
@section('hero')
<div class="hero-grid">
<div class="hero-card">
<h1>{{ $type === 'plugin' ? '插件目录' : '主题目录' }}</h1>
<p>支持按分类和关键词浏览 {{ $type === 'plugin' ? '插件' : '主题' }},这里只展示信息,不直接提供下载地址。</p>
</div>
<div class="hero-card">
<form method="get" class="toolbar" style="margin:0">
<div class="filters" style="width:100%">
<div class="field" style="flex:1 1 220px">
<label>关键词</label>
<input class="input" type="text" name="keyword" value="{{ $filters['keyword'] }}" placeholder="搜索名称、slug 或摘要">
</div>
<div class="field">
<label>分类</label>
<select class="select" name="category">
<option value="">全部</option>
@foreach($categories as $category)
<option value="{{ $category->slug }}" @selected($filters['category'] === $category->slug)>{{ $category->name }}</option>
@endforeach
</select>
</div>
<div class="field">
<label>排序</label>
<select class="select" name="sort">
<option value="latest" @selected($filters['sort'] === 'latest')>最新更新</option>
<option value="name" @selected($filters['sort'] === 'name')>按名称</option>
</select>
</div>
<section class="hero">
<div class="container">
<span class="eyebrow">{{ $typeLabel }}</span>
<h1 class="headline">{{ $typeLabel }}目录</h1>
<p class="lede">统一浏览 {{ $typeLabel }} 的摘要、分类、作者和版本,筛选逻辑保持轻量,适合做公开展示与目录导航。</p>
<form method="get" class="filter-bar">
<div class="field" style="flex:2 1 260px">
<label>关键词</label>
<input class="input" type="text" name="keyword" value="{{ $filters['keyword'] }}" placeholder="搜索名称、slug 或摘要">
</div>
<div class="card-actions">
<button class="btn" type="submit">筛选</button>
<div class="field" style="flex:1 1 180px">
<label>分类</label>
<select class="select" name="category">
<option value="">全部</option>
@foreach($categories as $category)
<option value="{{ $category->slug }}" @selected($filters['category'] === $category->slug)>{{ $category->name }}</option>
@endforeach
</select>
</div>
<div class="field" style="flex:1 1 160px">
<label>排序</label>
<select class="select" name="sort">
<option value="latest" @selected($filters['sort'] === 'latest')>最近更新</option>
<option value="name" @selected($filters['sort'] === 'name')>按名称</option>
</select>
</div>
<button class="btn" type="submit">应用筛选</button>
<a class="btn ghost" href="{{ route($type === 'plugin' ? 'storefront.plugins' : 'storefront.themes') }}">重置</a>
</form>
</div>
</div>
</section>
@endsection
@section('content')
<div class="panel">
<div class="section-head">
<div>
<h2>{{ $type === 'plugin' ? '插件列表' : '主题列表' }}</h2>
<p>共 {{ $packages->total() }} 项结果</p>
</div>
</div>
<div class="grid">
@forelse($packages as $package)
<div class="card">
<div class="card-top">
@if($package->icon_url)
<img class="icon" src="{{ $package->icon_url }}" alt="{{ $package->name }}">
@else
<div class="icon"></div>
@endif
<div>
<div class="title">{{ $package->name }}</div>
<div class="muted">{{ $package->author ?: '未知作者' }} · {{ $package->latestStableVersion?->version ?: $package->latest_version }}</div>
</div>
</div>
<div class="summary">{{ $package->summary ?: '暂无摘要' }}</div>
<div class="tags">
@foreach($package->categories as $category)
<span class="chip">{{ $category->name }}</span>
@endforeach
</div>
<div class="card-actions">
<a class="btn secondary" href="{{ route('storefront.show', [$package->type, $package->slug]) }}">查看详情</a>
</div>
<section class="section">
<div class="container">
<div class="section-head">
<div>
<span class="section-kicker">Catalog</span>
<h2>{{ $typeLabel }}列表</h2>
<p>共找到 {{ $packages->total() }} 个结果 · 当前分类 {{ $selectedCategory ?: '全部' }} · {{ $sortLabel }}。</p>
</div>
@empty
<div class="empty">当前没有符合条件的{{ $type === 'plugin' ? '插件' : '主题' }}。</div>
@endforelse
</div>
<div class="grid">
@forelse($packages as $package)
@include('storefront.partials.card', ['package' => $package, 'typeLabel' => $typeLabel, 'fallback' => $fallback])
@empty
<div class="empty">当前没有符合条件的{{ $typeLabel }}。</div>
@endforelse
</div>
{{ $packages->onEachSide(1)->links() }}
</div>
<div style="margin-top:18px">{{ $packages->links() }}</div>
</div>
</section>
@endsection
@@ -0,0 +1,26 @@
<article class="card">
<div class="card-top">
@if($package->icon_url)
<img class="card-icon" src="{{ $package->icon_url }}" alt="{{ $package->name }}">
@else
<div class="card-icon card-placeholder">{{ $fallback }}</div>
@endif
<div>
<h3 class="card-title">{{ $package->name }}</h3>
<p class="card-meta">{{ $package->author ?: '未知作者' }} · v{{ $package->latestStableVersion?->version ?: $package->latest_version ?: '-' }}</p>
</div>
</div>
<p class="card-summary">{{ $package->summary ?: '暂无摘要。' }}</p>
<div class="tags">
@foreach($package->categories as $category)
<span class="tag">{{ $category->name }}</span>
@endforeach
@if($package->is_featured)
<span class="tag solid">推荐</span>
@endif
</div>
<div class="card-foot">
<span class="dl">下载 {{ $package->download_count }}</span>
<a class="btn ghost sm" href="{{ route($package->type === 'plugin' ? 'storefront.plugin.show' : 'storefront.theme.show', ['slug' => $package->slug]) }}">查看详情</a>
</div>
</article>
+131 -86
View File
@@ -1,99 +1,144 @@
@extends('storefront.layout', ['title' => 'Tstore · ' . $package->name])
@extends('storefront.layout', ['title' => (config('app.name', 'Tstore')) . ' · ' . $package->name])
@php
$typeLabel = $package->type === 'plugin' ? '插件' : '主题';
$listRoute = $package->type === 'plugin' ? 'storefront.plugins' : 'storefront.themes';
$latestVersion = collect($detail['versions'])->firstWhere('is_latest', true)
?: collect($detail['versions'])->firstWhere('is_stable', true)
?: ($detail['versions'][0] ?? null);
@endphp
@section('hero')
<div class="hero-grid">
<div class="hero-card">
<h1>{{ $detail['name'] }}</h1>
<p>{{ $detail['summary'] ?: '暂无摘要' }}</p>
<div class="hero-meta">
<div class="hero-stat"><strong>{{ $detail['type'] }}</strong><span>类型</span></div>
<div class="hero-stat"><strong>{{ $detail['download_count'] }}</strong><span>累计下载</span></div>
<div class="hero-stat"><strong>{{ count($detail['versions']) }}</strong><span>版本数</span></div>
<div class="hero-stat"><strong>{{ count($detail['categories']) }}</strong><span>分类</span></div>
<section class="hero">
<div class="container">
<span class="eyebrow">{{ $typeLabel }}</span>
<div class="detail-head" style="margin-top:16px">
@if($detail['icon_url'])
<img class="detail-icon" src="{{ $detail['icon_url'] }}" alt="{{ $detail['name'] }}">
@else
<div class="detail-icon detail-placeholder">{{ $package->type === 'plugin' ? 'P' : 'T' }}</div>
@endif
<div>
<h1 class="headline" style="margin:0;font-size:30px">{{ $detail['name'] }}</h1>
<p class="lede" style="margin-top:10px">{{ $detail['summary'] ?: '这个' . $typeLabel . '当前还没有摘要,详情页会继续保留版本、兼容性和截图信息。' }}</p>
<div class="tags">
@foreach($package->categories as $category)
<span class="tag">{{ $category->name }}</span>
@endforeach
@if($detail['is_featured'])
<span class="tag solid">推荐</span>
@endif
<span class="tag">{{ $detail['author'] ?: '未知作者' }}</span>
</div>
</div>
</div>
<div class="hero-actions">
<a class="btn ghost" href="{{ route($listRoute) }}">返回{{ $typeLabel }}目录</a>
@if($detail['homepage'])
<a class="btn" href="{{ $detail['homepage'] }}" target="_blank" rel="noreferrer">访问主页</a>
@endif
</div>
<div class="stats">
<div class="stat"><strong>{{ strtoupper($detail['type']) }}</strong><span>扩展类型</span></div>
<div class="stat"><strong>{{ $detail['download_count'] }}</strong><span>累计下载</span></div>
<div class="stat"><strong>{{ count($detail['versions']) }}</strong><span>版本记录</span></div>
<div class="stat"><strong>{{ $latestVersion['version'] ?? '-' }}</strong><span>当前主版本</span></div>
</div>
</div>
<div class="hero-card">
<h3 style="margin:0 0 10px;font-size:20px">说明</h3>
<p>这是前台展示页,只展示扩展信息、版本兼容性和截图,不直接暴露下载地址。需要维护请进入后台。</p>
<div class="card-actions" style="margin-top:18px">
<a class="btn secondary" href="{{ route('webadmin.login') }}">后台登录</a>
</div>
</div>
</div>
</section>
@endsection
@section('content')
<div class="detail-grid">
<div class="detail-card">
<h3>扩展说明</h3>
<div class="muted">作者:{{ $detail['author'] ?: '未知作者' }}</div>
<div class="muted">主页:{{ $detail['homepage'] ?: '未提供' }}</div>
<div class="tags">
@foreach($package->categories as $category)
<span class="chip">{{ $category->name }}</span>
@endforeach
</div>
<div style="margin-top:16px;white-space:pre-wrap">{{ $detail['description'] ?: '暂无详细描述。' }}</div>
</div>
<div class="detail-card">
<h3>版本信息</h3>
<div class="version-list">
@forelse($detail['versions'] as $version)
<div class="version">
<strong>v{{ $version['version'] }}</strong>
<div class="muted">{{ $version['is_latest'] ? '最新版本' : '历史版本' }} · {{ $version['is_stable'] ? '稳定版' : '预发布' }}</div>
<div class="muted">Typecho {{ $version['compatibility']['typecho_min'] ?: '-' }} ~ {{ $version['compatibility']['typecho_max'] ?: '-' }}</div>
<div class="muted">PHP {{ $version['compatibility']['php_min'] ?: '-' }} ~ {{ $version['compatibility']['php_max'] ?: '-' }}</div>
@if(!empty($version['changelog']))
<div style="margin-top:8px">{{ $version['changelog'] }}</div>
@php
$screenshots = $detail['screenshots'];
$featuredScreenshot = $screenshots[0] ?? null;
$secondaryScreenshots = array_slice($screenshots, 1);
@endphp
<section class="section">
<div class="container">
<div class="detail-layout">
<div class="stack">
<article class="info-card">
<span class="section-kicker">Preview</span>
<h3>预览截图</h3>
@if($featuredScreenshot)
<div class="shots">
<figure class="shot-feature">
<img src="{{ $featuredScreenshot['url'] }}" alt="{{ $featuredScreenshot['caption'] ?: $detail['name'] }}">
<figcaption>
<strong>{{ $featuredScreenshot['caption'] ?: $detail['name'] . ' 主预览图' }}</strong>
<span>第一张截图作为主视觉展示,后台可继续追加更多截图并调整排序。</span>
</figcaption>
</figure>
@if(count($secondaryScreenshots))
<div class="shot-grid">
@foreach($secondaryScreenshots as $shot)
<figure class="shot-card">
<img src="{{ $shot['url'] }}" alt="{{ $shot['caption'] ?: $detail['name'] }}">
<figcaption>{{ $shot['caption'] ?: $detail['name'] . ' 截图' }}</figcaption>
</figure>
@endforeach
</div>
@endif
</div>
@else
<div class="empty">当前还没有预览截图。</div>
@endif
</div>
@empty
<div class="empty">当前没有版本记录。</div>
@endforelse
</div>
</div>
</div>
@if(count($detail['screenshots']))
<div class="panel section">
<div class="section-head">
<div>
<h2>截图展示</h2>
<p>只展示视觉效果,不提供下载入口。</p>
</div>
</div>
<div class="gallery">
@foreach($detail['screenshots'] as $shot)
<div class="shot">
<img src="{{ $shot['url'] }}" alt="{{ $shot['caption'] ?: $detail['name'] }}">
<div>{{ $shot['caption'] ?: '扩展截图' }}</div>
</article>
</div>
@endforeach
</div>
</div>
@endif
<div class="panel section">
<div class="section-head">
<div>
<h2>更多{{ $package->type === 'plugin' ? '插件' : '主题' }}</h2>
<p>继续浏览同类内容。</p>
<aside class="stack">
<article class="info-card">
<h3>{{ $typeLabel }}说明</h3>
<div class="rich-text">{{ $detail['description'] ?: '当前还没有更详细的介绍。' }}</div>
</article>
<article class="info-card">
<h3>资料与兼容性</h3>
<div class="info-list">
<div class="info-item"><strong>作者与协议</strong><span>{{ $detail['author'] ?: '未知作者' }} · {{ $detail['license'] ?: '未声明 License' }}</span></div>
<div class="info-item"><strong>最近更新</strong><span>{{ optional($package->updated_at)->format('Y-m-d H:i') ?: '未记录' }}</span></div>
@if($detail['homepage'])
<div class="info-item"><strong>主页</strong><a href="{{ $detail['homepage'] }}" target="_blank" rel="noreferrer">{{ $detail['homepage'] }}</a></div>
@endif
</div>
</article>
<article class="info-card">
<h3>版本记录</h3>
<div class="version-list">
@forelse($detail['versions'] as $version)
<div class="version-item">
<div class="version-head">
<div>
<div class="version-title">v{{ $version['version'] }}</div>
<div class="version-meta">发布于 {{ $version['published_at'] ? \Illuminate\Support\Carbon::parse($version['published_at'])->format('Y-m-d H:i') : '未标记时间' }}</div>
</div>
<div class="tags" style="margin-top:0">
<span class="tag {{ $version['is_stable'] ? 'solid' : '' }}">{{ $version['is_stable'] ? '稳定版' : '预发布' }}</span>
@if($version['is_latest'])
<span class="tag">最新</span>
@endif
</div>
</div>
<div class="tags">
<span class="tag">Typecho {{ $version['compatibility']['typecho_min'] ?: '-' }} ~ {{ $version['compatibility']['typecho_max'] ?: '-' }}</span>
<span class="tag">PHP {{ $version['compatibility']['php_min'] ?: '-' }} ~ {{ $version['compatibility']['php_max'] ?: '-' }}</span>
@if(!empty($version['compatibility']['php_extensions']))
<span class="tag">扩展 {{ collect($version['compatibility']['php_extensions'])->join(', ') }}</span>
@endif
</div>
@if(!empty($version['changelog']))
<div class="rich-text" style="margin-top:12px;font-size:14px">{{ $version['changelog'] }}</div>
@endif
</div>
@empty
<div class="empty">当前没有版本记录。</div>
@endforelse
</div>
</article>
</aside>
</div>
</div>
<div class="grid">
@forelse($related as $item)
<div class="card">
<div class="title">{{ $item->name }}</div>
<div class="muted">{{ $item->summary ?: '暂无摘要' }}</div>
<div class="card-actions">
<a class="btn secondary" href="{{ route('storefront.show', [$item->type, $item->slug]) }}">查看详情</a>
</div>
</div>
@empty
<div class="empty">当前没有更多相关内容。</div>
@endforelse
</div>
</div>
</section>
@endsection