初始化

This commit is contained in:
2026-08-08 18:27:38 +08:00
parent efc150c937
commit 060eb79c09
336 changed files with 24613 additions and 15 deletions
@@ -0,0 +1,84 @@
@extends('admin.layout')
@php $editing = isset($article) && $article; @endphp
@section('title', $editing ? '编辑文章' : '新建文章')
@section('page-title', $editing ? '编辑文章' : '新建文章')
@section('content')
<form method="POST" action="{{ $editing ? route('admin.articles.update', $article) : route('admin.articles.store') }}" class="form-card">
@csrf
@if ($editing) @method('PUT') @endif
<div class="field">
<label>标题</label>
<input name="title" class="input" value="{{ old('title', $article?->title ?? '') }}" required>
</div>
<div class="field">
<label>Slug</label>
<input name="slug" class="input" value="{{ old('slug', $article?->slug ?? '') }}" required>
</div>
<div class="field">
<label>分类</label>
<select name="category_id" class="select">
<option value="">无分类</option>
@foreach ($categories as $c)
<option value="{{ $c->id }}" @selected(old('category_id', $article?->category_id) == $c->id)>{{ $c->name }}</option>
@endforeach
</select>
</div>
<div class="field">
<label>作者</label>
<input name="author" class="input" value="{{ old('author', $article?->author ?? '') }}">
</div>
<div class="field">
<label>摘要</label>
<input name="summary" class="input" value="{{ old('summary', $article?->summary ?? '') }}">
</div>
<div class="field">
<label>正文(富文本)</label>
<x-rich-editor name="body" :value="old('body', $article?->body ?? '')" />
</div>
<div class="field">
<label>封面图(可选,支持上传)</label>
<input type="file" id="cover-file" accept="image/*" style="margin-bottom:8px;">
<div style="display:flex; gap:8px; align-items:center;">
<input name="cover" id="cover-url" class="input" style="flex:1;" value="{{ old('cover', $article?->cover ?? '') }}" placeholder="图片地址(可上传或粘贴 URL">
<button type="button" class="btn" onclick="uploadFile('cover')">上传</button>
</div>
<p class="form-hint">仅「专题报道」版式会使用封面图。支持 jpg/png/gif/webp,单张 5MB。</p>
</div>
<div class="field">
<label>状态</label>
<select name="status" class="select">
<option value="approved" @selected(old('status', $article?->status ?? 'approved') === 'approved')>已发布</option>
<option value="draft" @selected(old('status', $article?->status ?? '') === 'draft')>草稿</option>
</select>
</div>
<div class="field">
<label>版式模板</label>
<select name="template" class="select">
@foreach (App\Models\Article::TEMPLATES as $key => $label)
<option value="{{ $key }}" @selected(old('template', $article?->template ?? 'standard') === $key)>{{ $label }}</option>
@endforeach
</select>
</div>
<div class="field">
<label>SEO 标题(留空则使用文章标题)</label>
<input name="seo_title" class="input" value="{{ old('seo_title', $article?->seo_title ?? '') }}">
</div>
<div class="field">
<label>SEO 描述(留空则使用摘要)</label>
<input name="seo_description" class="input" value="{{ old('seo_description', $article?->seo_description ?? '') }}">
</div>
<div class="field">
<label>发布时间</label>
<input type="date" name="published_at" class="input" value="{{ old('published_at', $article?->published_at?->format('Y-m-d')) }}">
</div>
<div class="form-actions">
<button class="btn btn-primary">保存</button>
<a href="{{ route('admin.articles.index') }}" class="btn">取消</a>
</div>
</form>
@endsection
@@ -0,0 +1,39 @@
@extends('admin.layout')
@section('title', '文章管理')
@section('page-title', '文章管理')
@section('actions')
<a href="{{ route('admin.articles.create') }}" class="btn btn-primary btn-sm"><x-icon name="plus" :size="16"/> 新建文章</a>
@endsection
@section('content')
<table class="data-table">
<thead><tr><th>标题</th><th>分类</th><th>状态</th><th>发布时间</th><th>操作</th></tr></thead>
<tbody>
@forelse ($articles as $a)
<tr>
<td>{{ $a->title }}</td>
<td>{{ $a->category?->name ?: '-' }}</td>
<td>
@if ($a->status === 'approved') 已发布
@elseif ($a->status === 'pending') <span class="text-warning">待审</span>
@elseif ($a->status === 'rejected') <span class="text-danger">已驳回</span>
@else 草稿
@endif
</td>
<td class="muted">{{ $a->published_at?->format('Y-m-d') ?: '-' }}</td>
<td class="row-actions">
<a href="{{ route('admin.articles.edit', $a) }}" class="btn btn-sm"><x-icon name="edit" :size="16"/></a>
<form method="POST" action="{{ route('admin.articles.destroy', $a) }}" onsubmit="return confirm('确认删除该文章?')">
@csrf @method('DELETE')
<button class="btn btn-sm btn-danger"><x-icon name="trash" :size="16"/></button>
</form>
</td>
</tr>
@empty
<tr><td colspan="5" class="muted">暂无文章。</td></tr>
@endforelse
</tbody>
</table>
{{ $articles->links('pagination::bootstrap-5') }}
@endsection
@@ -0,0 +1,38 @@
@extends('admin.layout')
@php $editing = isset($category) && $category; @endphp
@section('title', $editing ? '编辑分类' : '新建分类')
@section('page-title', $editing ? '编辑分类' : '新建分类')
@section('content')
<form method="POST" action="{{ $editing ? route('admin.categories.update', $category) : route('admin.categories.store') }}" class="form-card">
@csrf
@if ($editing) @method('PUT') @endif
<div class="field">
<label>名称</label>
<input name="name" class="input" value="{{ old('name', $category?->name ?? '') }}" required>
</div>
<div class="field">
<label>Slug</label>
<input name="slug" class="input" value="{{ old('slug', $category?->slug ?? '') }}" required>
</div>
<div class="field">
<label>类型</label>
<select name="type" class="select">
<option value="product" @selected(old('type', $category?->type ?? 'product') === 'product')>产品</option>
<option value="article" @selected(old('type', $category?->type ?? '') === 'article')>文章</option>
</select>
</div>
<div class="field">
<label>排序</label>
<input type="number" name="sort" class="input" value="{{ old('sort', $category?->sort ?? 0) }}">
</div>
<div class="form-actions">
<button class="btn btn-primary">保存</button>
<a href="{{ route('admin.categories.index') }}" class="btn">取消</a>
</div>
</form>
@endsection
@@ -0,0 +1,32 @@
@extends('admin.layout')
@section('title', '分类管理')
@section('page-title', '分类管理')
@section('actions')
<a href="{{ route('admin.categories.create') }}" class="btn btn-primary btn-sm"><x-icon name="plus" :size="16"/> 新建分类</a>
@endsection
@section('content')
<table class="data-table">
<thead><tr><th>名称</th><th>类型</th><th>排序</th><th class="col-op">操作</th></tr></thead>
<tbody>
@forelse ($categories as $c)
<tr>
<td>{{ $c->name }} <span class="muted">/ {{ $c->slug }}</span></td>
<td>{{ $c->type === 'product' ? '产品' : '文章' }}</td>
<td>{{ $c->sort }}</td>
<td class="row-actions">
<a href="{{ route('admin.categories.edit', $c) }}" class="btn btn-sm"><x-icon name="edit" :size="16"/></a>
<form method="POST" action="{{ route('admin.categories.destroy', $c) }}" onsubmit="return confirm('确认删除该分类?')">
@csrf @method('DELETE')
<button class="btn btn-sm btn-danger"><x-icon name="trash" :size="16"/></button>
</form>
</td>
</tr>
@empty
<tr><td colspan="4" class="muted">暂无分类。</td></tr>
@endforelse
</tbody>
</table>
{{ $categories->links('pagination::bootstrap-5') }}
@endsection
@@ -0,0 +1,30 @@
@extends('admin.layout')
@section('title', '留言管理')
@section('page-title', '留言管理')
@section('content')
<table class="data-table">
<thead><tr><th>姓名</th><th>邮箱</th><th>主题</th><th>时间</th><th class="col-op">操作</th></tr></thead>
<tbody>
@forelse ($messages as $m)
<tr>
<td>{{ $m->name }}</td>
<td>{{ $m->email }}</td>
<td>{{ $m->subject }}</td>
<td class="muted">{{ $m->created_at->format('Y-m-d') }}</td>
<td class="row-actions">
<a href="{{ route('admin.contacts.show', $m) }}" class="btn btn-sm"><x-icon name="edit" :size="16"/></a>
<form method="POST" action="{{ route('admin.contacts.destroy', $m) }}" onsubmit="return confirm('确认删除该留言?')">
@csrf @method('DELETE')
<button class="btn btn-sm btn-danger"><x-icon name="trash" :size="16"/></button>
</form>
</td>
</tr>
@empty
<tr><td colspan="5" class="muted">暂无留言。</td></tr>
@endforelse
</tbody>
</table>
{{ $messages->links('pagination::bootstrap-5') }}
@endsection
@@ -0,0 +1,24 @@
@extends('admin.layout')
@section('title', '留言详情')
@section('page-title', '留言详情')
@section('actions')
<a href="{{ route('admin.contacts.index') }}" class="btn btn-sm"> 返回列表</a>
@endsection
@section('content')
<div class="card">
<p><strong>姓名:</strong>{{ $contactMessage->name }}</p>
<p><strong>邮箱:</strong>{{ $contactMessage->email }}</p>
<p><strong>电话:</strong>{{ $contactMessage->phone ?: '-' }}</p>
<p><strong>主题:</strong>{{ $contactMessage->subject }}</p>
<p><strong>时间:</strong>{{ $contactMessage->created_at->format('Y-m-d H:i') }}</p>
<hr>
<p class="prose">{{ nl2br(e($contactMessage->message)) }}</p>
</div>
<form method="POST" action="{{ route('admin.contacts.destroy', $contactMessage) }}" onsubmit="return confirm('确认删除该留言?')">
@csrf @method('DELETE')
<button class="btn btn-danger">删除留言</button>
</form>
@endsection
+41
View File
@@ -0,0 +1,41 @@
@extends('admin.layout')
@section('title', '仪表盘')
@section('page-title', '仪表盘')
@section('content')
<div class="stat-grid">
<div class="stat-card"><span class="stat-num">{{ $stats['products'] }}</span><span class="stat-label">产品</span></div>
<div class="stat-card"><span class="stat-num">{{ $stats['articles'] }}</span><span class="stat-label">文章</span></div>
<div class="stat-card"><span class="stat-num">{{ $stats['categories'] }}</span><span class="stat-label">分类</span></div>
<div class="stat-card"><span class="stat-num">{{ $stats['glossary'] }}</span><span class="stat-label">术语</span></div>
<div class="stat-card"><span class="stat-num">{{ $stats['threads'] }}</span><span class="stat-label">论坛帖</span></div>
<div class="stat-card"><span class="stat-num">{{ $stats['users'] }}</span><span class="stat-label">用户</span></div>
<div class="stat-card"><span class="stat-num">{{ $stats['messages'] }}</span><span class="stat-label">留言</span></div>
</div>
<div class="admin-cols">
<div class="card">
<h2>最新留言</h2>
@forelse ($recentMessages as $m)
<div class="mini-row">
<div><strong>{{ $m->name }}</strong> <span class="muted">{{ $m->email }}</span></div>
<div class="muted">{{ Str::limit($m->message, 60) }}</div>
</div>
@empty
<p class="muted">暂无留言。</p>
@endforelse
</div>
<div class="card">
<h2>最新帖子</h2>
@forelse ($recentThreads as $t)
<div class="mini-row">
<div><strong>{{ $t->title }}</strong></div>
<div class="muted">{{ $t->user->name ?? '匿名' }} · {{ $t->board->name ?? '' }}</div>
</div>
@empty
<p class="muted">暂无帖子。</p>
@endforelse
</div>
</div>
@endsection
+56
View File
@@ -0,0 +1,56 @@
@extends('admin.layout')
@section('title', '数据库管理')
@section('page-title', '数据库管理')
@section('content')
<div class="card mb-6">
<h2>数据库信息</h2>
<div class="kv">
<div><span class="muted">连接</span><strong>{{ $connection }}</strong></div>
<div><span class="muted">数据库</span><strong>{{ $dbName }}</strong></div>
<div><span class="muted">已执行迁移</span><strong>{{ count($done) }}</strong></div>
<div><span class="muted">待执行迁移</span><strong class="@if(count($pending) > 0) text-warning @endif">{{ count($pending) }}</strong></div>
</div>
</div>
<div class="admin-cols">
<div class="card">
<h2>数据库升级</h2>
@if (count($pending) === 0)
<p class="muted">没有待执行的迁移,数据库已是最新。</p>
@else
<p class="muted">检测到 {{ count($pending) }} 个待执行迁移:</p>
<ul class="mig-list">
@foreach ($pending as $m)
<li><x-icon name="arrow" :size="16"/> {{ $m }}</li>
@endforeach
</ul>
@endif
<form method="POST" action="{{ route('admin.database.upgrade') }}" class="mt-4">
@csrf
<button type="submit" class="btn btn-primary">执行升级(migrate</button>
</form>
</div>
<div class="card">
<h2>已执行迁移({{ count($done) }}</h2>
@if (count($done) === 0)
<p class="muted">暂无已执行的迁移。</p>
@else
<ul class="mig-list done">
@foreach ($done as $m)
<li><x-icon name="check" :size="16"/> {{ $m }}</li>
@endforeach
</ul>
@endif
</div>
</div>
@if (session('migrate_output'))
<div class="card mt-6">
<h2>本次升级输出</h2>
<pre class="mig-output">{{ session('migrate_output') }}</pre>
</div>
@endif
@endsection
@@ -0,0 +1,35 @@
@extends('admin.layout')
@php $editing = isset($board) && $board; @endphp
@section('title', $editing ? '编辑版块' : '新建版块')
@section('page-title', $editing ? '编辑版块' : '新建版块')
@section('content')
<form method="POST" action="{{ $editing ? route('admin.forum.boards.update', $board) : route('admin.forum.boards.store') }}" class="form-card">
@csrf
@if ($editing) @method('PUT') @endif
<div class="field">
<label>名称</label>
<input name="name" class="input" value="{{ old('name', $board?->name ?? '') }}" required>
</div>
<div class="field">
<label>Slug</label>
<input name="slug" class="input" value="{{ old('slug', $board?->slug ?? '') }}" required>
</div>
<div class="field">
<label>描述</label>
<textarea name="description" class="textarea">{{ old('description', $board?->description ?? '') }}</textarea>
</div>
<div class="field">
<label>排序</label>
<input type="number" name="sort" class="input" value="{{ old('sort', $board?->sort ?? 0) }}">
</div>
<div class="form-actions">
<button class="btn btn-primary">保存</button>
<a href="{{ route('admin.forum.boards.index') }}" class="btn">取消</a>
</div>
</form>
@endsection
@@ -0,0 +1,32 @@
@extends('admin.layout')
@section('title', '论坛版块')
@section('page-title', '论坛版块')
@section('actions')
<a href="{{ route('admin.forum.boards.create') }}" class="btn btn-primary btn-sm"><x-icon name="plus" :size="16"/> 新建版块</a>
@endsection
@section('content')
<table class="data-table">
<thead><tr><th>名称</th><th>Slug</th><th>排序</th><th class="col-op">操作</th></tr></thead>
<tbody>
@forelse ($boards as $b)
<tr>
<td>{{ $b->name }}</td>
<td class="muted">{{ $b->slug }}</td>
<td>{{ $b->sort }}</td>
<td class="row-actions">
<a href="{{ route('admin.forum.boards.edit', $b) }}" class="btn btn-sm"><x-icon name="edit" :size="16"/></a>
<form method="POST" action="{{ route('admin.forum.boards.destroy', $b) }}" onsubmit="return confirm('确认删除该版块?帖子将一并删除。')">
@csrf @method('DELETE')<button class="btn btn-sm btn-danger"><x-icon name="trash" :size="16"/></button></form>
</td>
</tr>
@empty
<tr><td colspan="4" class="muted">暂无版块。</td></tr>
@endforelse
</tbody>
</table>
{{ $boards->links('pagination::bootstrap-5') }}
<p style="margin-top:16px;"><a href="{{ route('admin.forum.threads.index') }}" class="btn btn-sm">查看全部帖子 </a></p>
@endsection
@@ -0,0 +1,31 @@
@extends('admin.layout')
@section('title', '论坛帖子')
@section('page-title', '论坛帖子')
@section('content')
<table class="data-table">
<thead><tr><th>标题</th><th>版块</th><th>作者</th><th>时间</th><th class="col-op">操作</th></tr></thead>
<tbody>
@forelse ($threads as $t)
<tr>
<td>
@if ($t->is_pinned)<x-icon name="pin" :size="14"/> @endif
{{ $t->title }}
</td>
<td>{{ $t->board?->name ?: '-' }}</td>
<td>{{ $t->user?->name ?: '匿名' }}</td>
<td class="muted">{{ $t->created_at->format('Y-m-d') }}</td>
<td class="row-actions">
<a href="{{ route('forum.thread', $t) }}" class="btn btn-sm" target="_blank"><x-icon name="arrow" :size="16"/></a>
<form method="POST" action="{{ route('admin.forum.threads.destroy', $t) }}" onsubmit="return confirm('确认删除该帖子?')">
@csrf @method('DELETE')<button class="btn btn-sm btn-danger"><x-icon name="trash" :size="16"/></button></form>
</td>
</tr>
@empty
<tr><td colspan="5" class="muted">暂无帖子。</td></tr>
@endforelse
</tbody>
</table>
{{ $threads->links('pagination::bootstrap-5') }}
@endsection
@@ -0,0 +1,39 @@
@extends('admin.layout')
@php $editing = isset($term) && $term; @endphp
@section('title', $editing ? '编辑术语' : '新建术语')
@section('page-title', $editing ? '编辑术语' : '新建术语')
@section('content')
<form method="POST" action="{{ $editing ? route('admin.glossary.update', $term) : route('admin.glossary.store') }}" class="form-card">
@csrf
@if ($editing) @method('PUT') @endif
<div class="field">
<label>术语</label>
<input name="term" class="input" value="{{ old('term', $term?->term ?? '') }}" required>
</div>
<div class="field">
<label>Slug</label>
<input name="slug" class="input" value="{{ old('slug', $term?->slug ?? '') }}" required>
</div>
<div class="field">
<label>分类</label>
<input name="category" class="input" value="{{ old('category', $term?->category ?? '') }}">
</div>
<div class="field">
<label>定义</label>
<textarea name="definition" class="textarea" required>{{ old('definition', $term?->definition ?? '') }}</textarea>
</div>
<div class="field">
<label>别名(逗号分隔)</label>
<input name="aliases" class="input" value="{{ old('aliases', $term?->aliases ?? '') }}">
</div>
<div class="form-actions">
<button class="btn btn-primary">保存</button>
<a href="{{ route('admin.glossary.index') }}" class="btn">取消</a>
</div>
</form>
@endsection
@@ -0,0 +1,31 @@
@extends('admin.layout')
@section('title', '术语库管理')
@section('page-title', '术语库管理')
@section('actions')
<a href="{{ route('admin.glossary.create') }}" class="btn btn-primary btn-sm"><x-icon name="plus" :size="16"/> 新建术语</a>
@endsection
@section('content')
<table class="data-table">
<thead><tr><th>术语</th><th>分类</th><th class="col-op">操作</th></tr></thead>
<tbody>
@forelse ($terms as $t)
<tr>
<td>{{ $t->term }} <span class="muted">/ {{ $t->slug }}</span></td>
<td>{{ $t->category ?: '-' }}</td>
<td class="row-actions">
<a href="{{ route('admin.glossary.edit', $t) }}" class="btn btn-sm"><x-icon name="edit" :size="16"/></a>
<form method="POST" action="{{ route('admin.glossary.destroy', $t) }}" onsubmit="return confirm('确认删除该术语?')">
@csrf @method('DELETE')
<button class="btn btn-sm btn-danger"><x-icon name="trash" :size="16"/></button>
</form>
</td>
</tr>
@empty
<tr><td colspan="3" class="muted">暂无术语。</td></tr>
@endforelse
</tbody>
</table>
{{ $terms->links('pagination::bootstrap-5') }}
@endsection
@@ -0,0 +1,65 @@
@extends('admin.layout')
@php
$isEdit = isset($slide) && $slide;
@endphp
@section('title', $isEdit ? '编辑轮播项' : '新建轮播项')
@section('page-title', $isEdit ? '编辑轮播项' : '新建轮播项')
@section('actions')
<a href="{{ route('admin.hero-slides.index') }}" class="btn btn-sm">返回列表</a>
@endsection
@section('content')
<form method="POST" action="{{ $isEdit ? route('admin.hero-slides.update', $slide) : route('admin.hero-slides.store') }}">
@csrf
@if ($isEdit) @method('PUT') @endif
<div class="field">
<label>标题(后台标识 / 无障碍标题)*</label>
<input name="title" class="input" value="{{ old('title', $slide->title ?? '') }}" maxlength="120" required>
<p class="form-hint">仅用于后台识别与该区块的无障碍标题,不会作为大字标题显示(大字标题请在下方正文内排版)。</p>
</div>
<div class="field">
<label>Hero 正文(富文本)</label>
<x-rich-editor name="content" :value="old('content', $slide->content ?? '')" />
<p class="form-hint">使用富文本编辑主标题、副标题与说明文字;支持加粗、列表、引用、插入图片等。</p>
</div>
<div class="field">
<label>主按钮文字</label>
<input name="cta_text" class="input" value="{{ old('cta_text', $slide->cta_text ?? '') }}" maxlength="80" placeholder="例如:获取选型支持">
</div>
<div class="field">
<label>主按钮链接</label>
<input name="cta_url" class="input" value="{{ old('cta_url', $slide->cta_url ?? '') }}" maxlength="255" placeholder="/contact">
</div>
<div class="field">
<label>次按钮文字</label>
<input name="cta2_text" class="input" value="{{ old('cta2_text', $slide->cta2_text ?? '') }}" maxlength="80" placeholder="例如:浏览全部产品">
</div>
<div class="field">
<label>次按钮链接</label>
<input name="cta2_url" class="input" value="{{ old('cta2_url', $slide->cta2_url ?? '') }}" maxlength="255" placeholder="/products">
</div>
<div class="field">
<label>排序</label>
<input name="sort_order" type="number" class="input" value="{{ old('sort_order', $slide->sort_order ?? 0) }}" style="max-width:160px">
</div>
<div class="field">
<label class="inline-flex items-center gap-2">
<input type="checkbox" name="is_active" value="1" {{ old('is_active', $slide->is_active ?? true) ? 'checked' : '' }}>
启用该项
</label>
</div>
<div class="form-actions">
<button class="btn btn-primary">保存</button>
<a href="{{ route('admin.hero-slides.index') }}" class="btn">取消</a>
</div>
</form>
@endsection
@@ -0,0 +1,70 @@
@extends('admin.layout')
@section('title', '首页 Hero 轮播')
@section('page-title', '首页 Hero 轮播')
@section('actions')
<a href="{{ route('admin.hero-slides.create') }}" class="btn btn-primary btn-sm"><x-icon name="plus" :size="16"/> 新建轮播项</a>
@endsection
@section('content')
<div class="panel" style="margin-bottom:20px;padding:16px 18px;border:1px solid var(--color-border);border-radius:var(--radius-lg);background:var(--color-surface-2)">
<div class="flex items-center justify-between flex-wrap gap-3">
<div>
<div class="font-semibold text-fg">Hero 显示模式</div>
<div class="form-hint">轮播:启用项自动切换播放;独立单图:仅展示第一条启用项(不轮播)。</div>
</div>
<form method="POST" action="{{ route('admin.hero.mode') }}" class="flex gap-2">
@csrf
<button type="submit" name="hero_mode" value="carousel" class="btn btn-sm {{ $mode === 'carousel' ? 'btn-primary' : '' }}">轮播</button>
<button type="submit" name="hero_mode" value="single" class="btn btn-sm {{ $mode === 'single' ? 'btn-primary' : '' }}">独立单图</button>
</form>
</div>
</div>
<table class="data-table">
<thead>
<tr>
<th style="width:90px">排序</th>
<th style="width:90px">启用</th>
<th>标题(后台标识)</th>
<th class="col-op">操作</th>
</tr>
</thead>
<tbody>
@forelse ($slides as $slide)
<tr>
<td class="row-actions">
<form method="POST" action="{{ route('admin.hero-slides.move', $slide) }}" class="inline">
@csrf
<input type="hidden" name="direction" value="up">
<button class="btn btn-sm" title="上移" {{ $loop->first ? 'disabled' : '' }}><x-icon name="up" :size="16"/></button>
</form>
<form method="POST" action="{{ route('admin.hero-slides.move', $slide) }}" class="inline">
@csrf
<input type="hidden" name="direction" value="down">
<button class="btn btn-sm" title="下移" {{ $loop->last ? 'disabled' : '' }}><x-icon name="down" :size="16"/></button>
</form>
</td>
<td>
<form method="POST" action="{{ route('admin.hero-slides.toggle', $slide) }}" class="inline">
@csrf
<button class="btn btn-sm {{ $slide->is_active ? 'btn-primary' : '' }}" title="{{ $slide->is_active ? '点击停用' : '点击启用' }}">
{{ $slide->is_active ? '启用中' : '已停用' }}
</button>
</form>
</td>
<td>{{ $slide->title }}</td>
<td class="row-actions">
<a href="{{ route('admin.hero-slides.edit', $slide) }}" class="btn btn-sm"><x-icon name="edit" :size="16"/></a>
<form method="POST" action="{{ route('admin.hero-slides.destroy', $slide) }}" class="inline" onsubmit="return confirm('确认删除该轮播项?')">
@csrf @method('DELETE')
<button class="btn btn-sm btn-danger"><x-icon name="trash" :size="16"/></button>
</form>
</td>
</tr>
@empty
<tr><td colspan="4" class="muted">暂无轮播项,请点击右上角「新建轮播项」。</td></tr>
@endforelse
</tbody>
</table>
@endsection
+46
View File
@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>@yield('title', '管理后台') - 云创芯</title>
<link rel="stylesheet" href="{{ asset('css/app.css') }}?v={{ filemtime(public_path('css/app.css')) }}">
</head>
<body class="admin-body">
<div class="admin-shell">
<aside class="admin-side">
<div class="admin-brand"><img src="{{ asset('images/logo-dark.png') }}" alt="logo" class="brand-logo" width="40" height="24"> 云创芯电子后台</div>
<nav class="admin-nav">
@canPerm('dashboard.view')<a href="{{ route('admin.dashboard') }}" class="@if(request()->routeIs('admin.dashboard')) active @endif"><x-icon name="grid" :size="18"/> 仪表盘</a>@endcanPerm
@canPerm('products.view')<a href="{{ route('admin.products.index') }}" class="@if(request()->routeIs('admin.products.*')) active @endif"><x-icon name="box" :size="18"/> 产品</a>@endcanPerm
@canPerm('categories.view')<a href="{{ route('admin.categories.index') }}" class="@if(request()->routeIs('admin.categories.*')) active @endif"><x-icon name="tag" :size="18"/> 分类</a>@endcanPerm
@canPerm('articles.view')<a href="{{ route('admin.articles.index') }}" class="@if(request()->routeIs('admin.articles.*')) active @endif"><x-icon name="doc" :size="18"/> 文章</a>@endcanPerm
@if (auth()->user()->canPerm('articles.moderate') || auth()->user()->canPerm('forum.moderate'))<a href="{{ route('admin.moderation.index') }}" class="@if(request()->routeIs('admin.moderation.*')) active @endif"><x-icon name="check" :size="18"/> 审核队列</a>@endif
@canPerm('glossary.view')<a href="{{ route('admin.glossary.index') }}" class="@if(request()->routeIs('admin.glossary.*')) active @endif"><x-icon name="book" :size="18"/> 术语库</a>@endcanPerm
@canPerm('forum.view')<a href="{{ route('admin.forum.boards.index') }}" class="@if(request()->routeIs('admin.forum.*')) active @endif"><x-icon name="chat" :size="18"/> 论坛</a>@endcanPerm
@canPerm('contacts.view')<a href="{{ route('admin.contacts.index') }}" class="@if(request()->routeIs('admin.contacts.*')) active @endif"><x-icon name="mail" :size="18"/> 留言</a>@endcanPerm
@canPerm('users.view')<a href="{{ route('admin.users.index') }}" class="@if(request()->routeIs('admin.users.*')) active @endif"><x-icon name="users" :size="18"/> 用户</a>@endcanPerm
@canPerm('settings.view')<a href="{{ route('admin.settings') }}" class="@if(request()->routeIs('admin.settings')) active @endif"><x-icon name="settings" :size="18"/> 站点设置</a>@endcanPerm
@canPerm('appearance.view')<a href="{{ route('admin.hero-slides.index') }}" class="@if(request()->routeIs('admin.hero-slides.*')) active @endif"><x-icon name="layers" :size="18"/> 首页轮播</a>@endcanPerm
@canPerm('appearance.view')<a href="{{ route('admin.menus.index') }}" class="@if(request()->routeIs('admin.menus.*')) active @endif"><x-icon name="menu" :size="18"/> 导航菜单</a>@endcanPerm
@canPerm('settings.update')<a href="{{ route('admin.database') }}" class="@if(request()->routeIs('admin.database*')) active @endif"><x-icon name="database" :size="18"/> 数据库</a>@endcanPerm
</nav>
<div class="admin-foot">
<a href="{{ url('/') }}" target="_blank"><x-icon name="arrow" :size="16"/> 访问前台</a>
<a href="{{ route('auth.settings') }}"><x-icon name="lock" :size="16"/> 修改密码</a>
<form method="POST" action="{{ url('/logout') }}">@csrf<button type="submit" class="btn-link">退出登录</button></form>
</div>
</aside>
<main class="admin-main">
<div class="admin-topbar">
<h1>@yield('page-title', '管理后台')</h1>
<div class="admin-actions">@yield('actions')</div>
</div>
@if (session('success'))<div class="alert alert-success"><x-icon name="check" :size="18"/> {{ session('success') }}</div>@endif
@if ($errors->any())<div class="alert alert-danger"><x-icon name="alert" :size="18"/> 表单填写有误,请检查后重试。</div>@endif
@yield('content')
</main>
</div>
</body>
</html>
@@ -0,0 +1,47 @@
@extends('admin.layout')
@php
$isEdit = isset($menu) && $menu;
@endphp
@section('title', $isEdit ? '编辑菜单' : '新建菜单')
@section('page-title', $isEdit ? '编辑菜单' : '新建菜单')
@section('actions')
<a href="{{ route('admin.menus.index') }}" class="btn btn-sm">返回列表</a>
@endsection
@section('content')
<form method="POST" action="{{ $isEdit ? route('admin.menus.update', $menu) : route('admin.menus.store') }}">
@csrf
@if ($isEdit) @method('PUT') @endif
<div class="field">
<label>菜单文字 *</label>
<input name="label" class="input" value="{{ old('label', $menu->label ?? '') }}" placeholder="例如:产品服务" maxlength="60" required>
</div>
<div class="field">
<label>链接地址 *</label>
<input name="url" class="input" value="{{ old('url', $menu->url ?? '') }}" placeholder="站内路径如 /products 或完整外链 https://..." maxlength="255" required>
<p class="form-hint">站内页面填写相对路径(以 / 开头),外链填写完整 https:// 地址。</p>
</div>
<div class="field">
<label>排序</label>
<input name="sort_order" type="number" class="input" value="{{ old('sort_order', $menu->sort_order ?? 0) }}" style="max-width:160px">
<p class="form-hint">数字越小越靠前,相同则按添加先后排列。</p>
</div>
<div class="field">
<label class="inline-flex items-center gap-2">
<input type="checkbox" name="is_visible" value="1" {{ old('is_visible', $menu->is_visible ?? true) ? 'checked' : '' }}>
在前台显示
</label>
</div>
<div class="form-actions">
<button class="btn btn-primary">保存</button>
<a href="{{ route('admin.menus.index') }}" class="btn">取消</a>
</div>
</form>
@endsection
@@ -0,0 +1,60 @@
@extends('admin.layout')
@section('title', '导航菜单管理')
@section('page-title', '导航菜单管理')
@section('actions')
<a href="{{ route('admin.menus.create') }}" class="btn btn-primary btn-sm"><x-icon name="plus" :size="16"/> 新建菜单</a>
@endsection
@section('content')
<p class="form-hint mb-4">管理前台顶部导航菜单:可新增、编辑、删除,一键显示/隐藏,并通过上下移调整顺序。排序越小越靠前。</p>
<table class="data-table">
<thead>
<tr>
<th style="width:90px">排序</th>
<th style="width:90px">显示</th>
<th>菜单文字</th>
<th>链接</th>
<th class="col-op">操作</th>
</tr>
</thead>
<tbody>
@forelse ($menus as $menu)
<tr>
<td class="row-actions">
<form method="POST" action="{{ route('admin.menus.move', $menu) }}" class="inline">
@csrf
<input type="hidden" name="direction" value="up">
<button class="btn btn-sm" title="上移" {{ $loop->first ? 'disabled' : '' }}><x-icon name="up" :size="16"/></button>
</form>
<form method="POST" action="{{ route('admin.menus.move', $menu) }}" class="inline">
@csrf
<input type="hidden" name="direction" value="down">
<button class="btn btn-sm" title="下移" {{ $loop->last ? 'disabled' : '' }}><x-icon name="down" :size="16"/></button>
</form>
</td>
<td>
<form method="POST" action="{{ route('admin.menus.toggle', $menu) }}" class="inline">
@csrf
<button class="btn btn-sm {{ $menu->is_visible ? 'btn-primary' : '' }}" title="{{ $menu->is_visible ? '点击隐藏' : '点击显示' }}">
{{ $menu->is_visible ? '显示中' : '已隐藏' }}
</button>
</form>
</td>
<td>{{ $menu->label }}</td>
<td><code class="muted">{{ $menu->url }}</code></td>
<td class="row-actions">
<a href="{{ route('admin.menus.edit', $menu) }}" class="btn btn-sm"><x-icon name="edit" :size="16"/></a>
<form method="POST" action="{{ route('admin.menus.destroy', $menu) }}" class="inline" onsubmit="return confirm('确认删除该菜单项?')">
@csrf @method('DELETE')
<button class="btn btn-sm btn-danger"><x-icon name="trash" :size="16"/></button>
</form>
</td>
</tr>
@empty
<tr><td colspan="5" class="muted">暂无菜单项,请点击右上角「新建菜单」。</td></tr>
@endforelse
</tbody>
</table>
@endsection
@@ -0,0 +1,103 @@
@extends('admin.layout')
@section('title', '审核队列')
@section('page-title', '审核队列')
@section('actions')
@if ($canArticles)
<a href="{{ route('admin.moderation.index', ['tab' => 'articles']) }}" class="btn btn-sm @if($tab === 'articles') btn-primary @endif">文章投稿</a>
@endif
@if ($canForum)
<a href="{{ route('admin.moderation.index', ['tab' => 'threads']) }}" class="btn btn-sm @if($tab === 'threads') btn-primary @endif">论坛帖子</a>
<a href="{{ route('admin.moderation.index', ['tab' => 'replies']) }}" class="btn btn-sm @if($tab === 'replies') btn-primary @endif">回复</a>
@endif
@endsection
@section('content')
@if ($tab === 'articles' && $articles)
@if ($articles->isEmpty())
<p class="muted">暂无待审文章。</p>
@else
<table class="data-table">
<thead><tr><th>标题</th><th>投稿人</th><th>分类</th><th>摘要</th><th>操作</th></tr></thead>
<tbody>
@foreach ($articles as $a)
<tr>
<td>{{ $a->title }}</td>
<td>{{ $a->user?->name ?? '管理员' }}</td>
<td>{{ $a->category?->name ?: '-' }}</td>
<td class="muted">{{ Str::limit(strip_tags($a->summary ?: $a->body), 40) }}</td>
<td class="row-actions">
<form method="POST" action="{{ route('admin.moderation.articles.approve', $a) }}" style="display:inline">
@csrf<button class="btn btn-sm btn-primary">通过</button>
</form>
<form method="POST" action="{{ route('admin.moderation.articles.reject', $a) }}" style="display:inline">
@csrf<button class="btn btn-sm btn-danger">驳回</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
{{ $articles->links('pagination::bootstrap-5') }}
@endif
@elseif ($tab === 'threads' && $threads)
@if ($threads->isEmpty())
<p class="muted">暂无待审帖子。</p>
@else
<table class="data-table">
<thead><tr><th>标题</th><th>作者</th><th>板块</th><th>内容</th><th>操作</th></tr></thead>
<tbody>
@foreach ($threads as $t)
<tr>
<td>{{ $t->title }}</td>
<td>{{ $t->user?->name ?? '-' }}</td>
<td>{{ $t->board?->name ?: '-' }}</td>
<td class="muted">{{ Str::limit(strip_tags($t->body), 40) }}</td>
<td class="row-actions">
<form method="POST" action="{{ route('admin.moderation.threads.approve', $t) }}" style="display:inline">
@csrf<button class="btn btn-sm btn-primary">通过</button>
</form>
<form method="POST" action="{{ route('admin.moderation.threads.reject', $t) }}" style="display:inline">
@csrf<button class="btn btn-sm btn-danger">驳回</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
{{ $threads->links('pagination::bootstrap-5') }}
@endif
@elseif ($tab === 'replies' && $replies)
@if ($replies->isEmpty())
<p class="muted">暂无待审回复。</p>
@else
<table class="data-table">
<thead><tr><th>回复内容</th><th>作者</th><th>所属帖子</th><th>操作</th></tr></thead>
<tbody>
@foreach ($replies as $r)
<tr>
<td class="muted">{{ Str::limit(strip_tags($r->body), 60) }}</td>
<td>{{ $r->user?->name ?? '-' }}</td>
<td>{{ $r->thread?->title ?: '-' }}</td>
<td class="row-actions">
<form method="POST" action="{{ route('admin.moderation.replies.approve', $r) }}" style="display:inline">
@csrf<button class="btn btn-sm btn-primary">通过</button>
</form>
<form method="POST" action="{{ route('admin.moderation.replies.reject', $r) }}" style="display:inline">
@csrf<button class="btn btn-sm btn-danger">驳回</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
{{ $replies->links('pagination::bootstrap-5') }}
@endif
@else
<p class="muted">暂无待审内容。</p>
@endif
@endsection
@@ -0,0 +1,68 @@
@extends('admin.layout')
@php $editing = isset($product) && $product; @endphp
@section('title', $editing ? '编辑产品' : '新建产品')
@section('page-title', $editing ? '编辑产品' : '新建产品')
@section('content')
<form method="POST" action="{{ $editing ? route('admin.products.update', $product) : route('admin.products.store') }}" class="form-card">
@csrf
@if ($editing) @method('PUT') @endif
<div class="field">
<label>名称</label>
<input name="name" class="input" value="{{ old('name', $product?->name ?? '') }}" required>
</div>
<div class="field">
<label>Slug</label>
<input name="slug" class="input" value="{{ old('slug', $product?->slug ?? '') }}" required>
</div>
<div class="field">
<label>型号</label>
<input name="model" class="input" value="{{ old('model', $product?->model ?? '') }}">
</div>
<div class="field">
<label>分类</label>
<select name="category_id" class="select">
<option value="">无分类</option>
@foreach ($categories as $c)
<option value="{{ $c->id }}" @selected(old('category_id', $product?->category_id) == $c->id)>{{ $c->name }}</option>
@endforeach
</select>
</div>
<div class="field">
<label>简介</label>
<input name="summary" class="input" value="{{ old('summary', $product?->summary ?? '') }}">
</div>
<div class="field">
<label>描述</label>
<textarea name="description" class="textarea">{{ old('description', $product?->description ?? '') }}</textarea>
</div>
<div class="field">
<label>规格参数</label>
<textarea name="specs" class="textarea">{{ old('specs', $product?->specs ?? '') }}</textarea>
<p class="form-hint">每行一个参数,或自由文本。</p>
</div>
<div class="field">
<label>封面图 URL</label>
<input name="cover" class="input" value="{{ old('cover', $product?->cover ?? '') }}">
</div>
<div class="field">
<label>状态</label>
<select name="status" class="select">
<option value="published" @selected(old('status', $product?->status ?? 'published') === 'published')>已发布</option>
<option value="draft" @selected(old('status', $product?->status ?? '') === 'draft')>草稿</option>
</select>
</div>
<div class="field">
<label>排序</label>
<input type="number" name="sort" class="input" value="{{ old('sort', $product?->sort ?? 0) }}">
</div>
<div class="form-actions">
<button class="btn btn-primary">保存</button>
<a href="{{ route('admin.products.index') }}" class="btn">取消</a>
</div>
</form>
@endsection
@@ -0,0 +1,33 @@
@extends('admin.layout')
@section('title', '产品管理')
@section('page-title', '产品管理')
@section('actions')
<a href="{{ route('admin.products.create') }}" class="btn btn-primary btn-sm"><x-icon name="plus" :size="16"/> 新建产品</a>
@endsection
@section('content')
<table class="data-table">
<thead><tr><th>名称</th><th>型号</th><th>分类</th><th>状态</th><th class="col-op">操作</th></tr></thead>
<tbody>
@forelse ($products as $p)
<tr>
<td>{{ $p->name }}</td>
<td class="muted">{{ $p->model ?: '-' }}</td>
<td>{{ $p->category?->name ?: '-' }}</td>
<td>{{ $p->status === 'published' ? '已发布' : '草稿' }}</td>
<td class="row-actions">
<a href="{{ route('admin.products.edit', $p) }}" class="btn btn-sm"><x-icon name="edit" :size="16"/></a>
<form method="POST" action="{{ route('admin.products.destroy', $p) }}" onsubmit="return confirm('确认删除该产品?')">
@csrf @method('DELETE')
<button class="btn btn-sm btn-danger"><x-icon name="trash" :size="16"/></button>
</form>
</td>
</tr>
@empty
<tr><td colspan="5" class="muted">暂无产品。</td></tr>
@endforelse
</tbody>
</table>
{{ $products->links('pagination::bootstrap-5') }}
@endsection
+105
View File
@@ -0,0 +1,105 @@
@extends('admin.layout')
@section('title', '站点设置')
@section('page-title', '站点设置')
@section('content')
<form method="POST" action="{{ route('admin.settings.update') }}" class="form-card">
@csrf
@method('PUT')
<div class="field">
<label>站点名称</label>
<input name="site_name" class="input" value="{{ old('site_name', $settings['site_name'] ?? '') }}" required>
</div>
<div class="field">
<label>站点描述(默认 description</label>
<input name="site_description" id="site_description" class="input" value="{{ old('site_description', $settings['site_description'] ?? '') }}">
<p class="form-hint">搜索结果摘要,建议 160 字符(超出会被截断)。<span class="char-count" id="cnt-site_description">0 / 160</span></p>
</div>
<div class="field">
<label>SEO 默认标题</label>
<input name="seo_title_default" id="seo_title_default" class="input" value="{{ old('seo_title_default', $settings['seo_title_default'] ?? '') }}">
<p class="form-hint">浏览器标签与搜索结果标题,建议 60 字符(超出会被截断)。<span class="char-count" id="cnt-seo_title_default">0 / 60</span></p>
</div>
<div class="field">
<label>SEO 默认描述</label>
<input name="seo_description_default" id="seo_description_default" class="input" value="{{ old('seo_description_default', $settings['seo_description_default'] ?? '') }}">
<p class="form-hint">全站默认 meta description,建议 160 字符。<span class="char-count" id="cnt-seo_description_default">0 / 160</span></p>
</div>
<div class="field">
<label>ICP 备案号</label>
<input name="icp_no" class="input" value="{{ old('icp_no', $settings['icp_no'] ?? '') }}" placeholder="粤ICP备xxxxx号-x">
<p class="form-hint">工信部备案号,将链接至 https://beian.miit.gov.cn/</p>
</div>
<div class="field">
<label>公安网备号(联网备案)</label>
<input name="security_filing_no" class="input" value="{{ old('security_filing_no', $settings['security_filing_no'] ?? '') }}" placeholder="粤公网安备XXXXXXXXXXXX号">
<p class="form-hint">按公安局要求悬挂联网备案号,格式「X公网安备XXXXXXXXXXXX号」,将链接至 https://beian.mps.gov.cn/</p>
</div>
<div class="field">
<label>Logo 地址(站内横版展示)</label>
<input name="logo_url" class="input" value="{{ old('logo_url', $settings['logo_url'] ?? '') }}" placeholder="/images/logo-dark.png">
<p class="form-hint">建议用 PNG,高度 56px2x 高清屏适配),置于 public/images/ 下。</p>
</div>
<div class="field">
<label>主题配色(前台)</label>
@php
$themeSwatches = ['azure' => '#4a8af4', 'slate' => '#6e8cb0', 'indigo' => '#7b7ef0', 'teal' => '#2bb6a4', 'amber' => '#e8a13c'];
$themeLabels = ['azure' => '沉稳蓝(默认)', 'slate' => '沉静灰蓝', 'indigo' => '蓝紫', 'teal' => '青绿', 'amber' => '暖琥珀'];
$currentTheme = $settings['theme'] ?? 'azure';
@endphp
<div class="theme-options">
@foreach ($themeSwatches as $key => $color)
<label class="theme-opt @if($currentTheme === $key) active @endif">
<input type="radio" name="theme" value="{{ $key }}" @if($currentTheme === $key) checked @endif>
<span class="theme-swatch" style="background: {{ $color }};"></span>
<span class="theme-label">{{ $themeLabels[$key] }}</span>
</label>
@endforeach
</div>
<p class="form-hint">切换后前台主色与辉光(Hero 极光、按钮、卡片高亮)整体跟随变化,保存并刷新即生效。</p>
</div>
<div class="field">
<label>Favicon 地址(浏览器标签图标)</label>
<input name="favicon_url" class="input" value="{{ old('favicon_url', $settings['favicon_url'] ?? '') }}" placeholder="/images/ccc-icon.ico">
<p class="form-hint">建议用方形 ICO 32×32 PNG。</p>
</div>
<div class="field">
<label>联系邮箱</label>
<input name="contact_email" class="input" value="{{ old('contact_email', $settings['contact_email'] ?? '') }}">
</div>
<div class="field">
<label>页脚自定义内容(富文本)</label>
<x-rich-editor name="footer_content" :value="old('footer_content', $settings['footer_content'] ?? '')" />
<p class="form-hint">支持标题、列表、链接、图片等,用于自由排版页脚信息(如友情链接、资质说明、客服二维码)。留空则前台页脚不显示该区块。</p>
</div>
<div class="form-actions">
<button class="btn btn-primary">保存设置</button>
</div>
</form>
<script>
(function () {
var limits = {
'seo_title_default': 60,
'seo_description_default': 160,
'site_description': 160
};
Object.keys(limits).forEach(function (id) {
var el = document.getElementById(id);
var cnt = document.getElementById('cnt-' + id);
if (!el || !cnt) return;
var max = limits[id];
function update() {
var len = (el.value || '').length;
cnt.textContent = len + ' / ' + max;
cnt.classList.toggle('over', len > max);
}
el.addEventListener('input', update);
update();
});
})();
</script>
@endsection
@@ -0,0 +1,87 @@
@extends('admin.layout')
@section('title', '新建用户')
@section('page-title', '新建用户')
@section('content')
<div class="card" style="max-width: 760px;">
<div class="card-body">
<form method="POST" action="{{ route('admin.users.store') }}">
@csrf
<div class="form-group">
<label class="form-label">姓名</label>
<input type="text" name="name" value="{{ old('name') }}" class="input" required>
</div>
<div class="form-group">
<label class="form-label">邮箱(登录账号)</label>
<input type="email" name="email" value="{{ old('email') }}" class="input" required>
</div>
<div class="form-group">
<label class="form-label">初始密码</label>
<input type="password" name="password" class="input" required autocomplete="new-password">
<p class="role-hint">至少 8 位。创建后可在用户编辑页随时重置。</p>
</div>
<div class="form-group">
<label class="form-label">角色</label>
<select name="role" id="role-select" class="input">
@foreach ($roles as $key => $cfg)
<option value="{{ $key }}"
{{ old('role', 'staff') === $key ? 'selected' : '' }}
@if ($key === 'super_admin' && ! auth()->user()->isSuperAdmin()) disabled @endif>
{{ $cfg['label'] }}
</option>
@endforeach
</select>
<p class="role-hint">
@if (! auth()->user()->isSuperAdmin())
只能创建权限低于自身的角色;超级管理员角色仅限超级管理员创建。
@endif
</p>
</div>
{{-- 内部员工:细粒度勾选 --}}
<div id="perm-section" class="perm-group" @if (old('role', 'staff') !== 'staff') style="display:none;" @endif>
<div class="perm-group-title">内部员工权限分配</div>
<p class="role-hint">勾选该员工可执行的权限点,未勾选则无对应后台访问权。</p>
@foreach ($groups as $gKey => $gLabel)
<div class="perm-group" style="margin-bottom: var(--space-3);">
<div class="perm-group-title">{{ $gLabel }}</div>
<div class="perm-list">
@foreach ($permissions as $permKey => $permCfg)
@if ($permCfg['group'] === $gKey)
<label class="perm-item">
<input type="checkbox" name="permissions[]" value="{{ $permKey }}"
{{ in_array($permKey, old('permissions', []), true) ? 'checked' : '' }}>
{{ $permCfg['label'] }}
</label>
@endif
@endforeach
</div>
</div>
@endforeach
</div>
<div class="row-actions" style="margin-top: var(--space-5);">
<button type="submit" class="btn btn-primary">创建用户</button>
<a href="{{ route('admin.users.index') }}" class="btn">返回</a>
</div>
</form>
</div>
</div>
<script>
(function () {
var sel = document.getElementById('role-select');
var sec = document.getElementById('perm-section');
function sync() {
sec.style.display = (sel.value === 'staff') ? '' : 'none';
}
sel.addEventListener('change', sync);
sync();
})();
</script>
@endsection
+143
View File
@@ -0,0 +1,143 @@
@extends('admin.layout')
@section('title', '编辑用户 - ' . $user->name)
@section('page-title', '编辑用户:' . $user->name)
@section('content')
<div class="card" style="max-width: 760px;">
<div class="card-body">
<div class="muted" style="margin-bottom: var(--space-4);">
邮箱:{{ $user->email }}
@if ($user->id === request()->user()->id)
<span class="badge badge-role-user">当前账号</span>
@endif
<span class="badge badge-role-{{ $user->role }}">{{ $user->roleLabel() }}</span>
</div>
@if (! $canManage)
<div class="alert alert-warning">您的权限不高于该用户,无法修改其角色、权限或密码。</div>
@endif
{{-- 角色 / 权限(仅具备分配权限且层级更高者可改) --}}
@if ($canManage && auth()->user()->canPerm('users.assign_role'))
<form method="POST" action="{{ route('admin.users.update', $user) }}">
@csrf
@method('PUT')
<div class="form-group">
<label class="form-label">角色</label>
<select name="role" id="role-select" class="input">
@foreach ($roles as $key => $cfg)
<option value="{{ $key }}"
{{ $user->role === $key ? 'selected' : '' }}
@if ($key === 'super_admin' && ! request()->user()->isSuperAdmin()) disabled @endif>
{{ $cfg['label'] }}
</option>
@endforeach
</select>
<p class="role-hint">
@if (! request()->user()->isSuperAdmin())
超级管理员角色仅限超级管理员分配;您只能分配权限低于自身的角色。
@else
不同角色拥有不同的后台视图与权限,下方将实时显示该角色的默认权限。
@endif
</p>
</div>
{{-- 各角色默认权限(只读,前端按所选角色切换显隐) --}}
<div id="role-defaults">
@foreach ($roles as $key => $cfg)
<div class="perm-group role-default" data-role="{{ $key }}"
style="background: var(--color-surface); {{ $user->role === $key ? '' : 'display:none;' }}">
<div class="perm-group-title">{{ $cfg['label'] }} · 默认权限</div>
<div class="perm-list">
@if (($cfg['permissions'] ?? []) === ['*'])
<div class="perm-item"><x-icon name="lock" :size="16"/> 全部权限(超级管理员)</div>
@elseif (empty($cfg['permissions'] ?? []))
<div class="perm-item muted">无(注册用户 / 待分配)</div>
@else
@foreach ($cfg['permissions'] as $p)
<div class="perm-item"><x-icon name="check" :size="16"/> {{ $permissions[$p]['label'] ?? $p }}</div>
@endforeach
@endif
</div>
</div>
@endforeach
</div>
{{-- 内部员工:细粒度勾选 --}}
<div id="perm-section" class="perm-group" @if ($user->role !== 'staff') style="display:none;" @endif>
<div class="perm-group-title">内部员工权限分配</div>
<p class="role-hint">勾选该员工可执行的权限点,未勾选则无对应后台访问权。</p>
@foreach ($groups as $gKey => $gLabel)
<div class="perm-group" style="margin-bottom: var(--space-3);">
<div class="perm-group-title">{{ $gLabel }}</div>
<div class="perm-list">
@foreach ($permissions as $permKey => $permCfg)
@if ($permCfg['group'] === $gKey)
<label class="perm-item">
<input type="checkbox" name="permissions[]" value="{{ $permKey }}"
{{ in_array($permKey, $userPerms, true) ? 'checked' : '' }}>
{{ $permCfg['label'] }}
</label>
@endif
@endforeach
</div>
</div>
@endforeach
</div>
<div class="row-actions" style="margin-top: var(--space-5);">
<button type="submit" class="btn btn-primary">保存角色与权限</button>
<a href="{{ route('admin.users.index') }}" class="btn">返回</a>
</div>
</form>
@endif
{{-- 重置密码(仅具备重置权限且层级更高者可操作) --}}
@if ($canManage && auth()->user()->canPerm('users.reset_password'))
<hr style="margin: var(--space-6) 0; border-color: var(--color-border);">
<div id="reset-password" class="perm-group">
<div class="perm-group-title">重置密码</div>
<p class="role-hint">可指定一个新密码,或留空由系统生成 12 位随机密码(生成后请妥善告知用户)。</p>
<form method="POST" action="{{ route('admin.users.reset-password', $user) }}" class="space-y-4">
@csrf
<div class="form-group">
<label class="form-label">新密码(可选)</label>
<input type="password" name="password" class="input" autocomplete="new-password" placeholder="留空则生成随机密码">
</div>
<div class="form-group">
<label class="form-label">确认新密码</label>
<input type="password" name="password_confirmation" class="input" autocomplete="new-password">
</div>
<button type="submit" class="btn btn-primary">重置密码</button>
</form>
</div>
@endif
</div>
</div>
@if ($canManage && auth()->user()->canPerm('users.assign_role'))
<script>
(function () {
var sel = document.getElementById('role-select');
var defaults = document.querySelectorAll('#role-defaults .role-default');
var sec = document.getElementById('perm-section');
function sync() {
var v = sel.value;
if (v === 'staff') {
sec.style.display = '';
defaults.forEach(function (el) { el.style.display = 'none'; });
} else {
sec.style.display = 'none';
defaults.forEach(function (el) {
el.style.display = (el.getAttribute('data-role') === v) ? '' : 'none';
});
}
}
sel.addEventListener('change', sync);
sync();
})();
</script>
@endif
@endsection
@@ -0,0 +1,58 @@
@extends('admin.layout')
@section('title', '用户管理')
@section('page-title', '用户管理')
@section('actions')
@canPerm('users.create')
<a href="{{ route('admin.users.create') }}" class="btn btn-primary"><x-icon name="plus" :size="16"/> 新建用户</a>
@endcanPerm
@endsection
@section('content')
<form method="GET" class="filter-bar">
<input type="text" name="q" value="{{ $q }}" class="input" placeholder="搜索姓名、邮箱...">
<button type="submit" class="btn btn-primary">查询</button>
</form>
<table class="data-table" style="margin-top:16px;">
<thead><tr><th>姓名</th><th>邮箱</th><th>角色</th><th>积分 / 等级</th><th class="col-op">操作</th></tr></thead>
<tbody>
@forelse ($users as $u)
@php
$me = auth()->user();
$canManage = $me->isSuperAdmin() || $me->roleLevel() > $u->roleLevel();
@endphp
<tr>
<td>{{ $u->name }}</td>
<td>{{ $u->email }}</td>
<td>
<span class="badge badge-role-{{ $u->role }}">{{ $u->roleLabel() }}</span>
@if ($u->id === $me->id)<span class="muted">(当前)</span>@endif
</td>
<td class="muted">{{ $u->points ?? 0 }} · L{{ $u->level() }}</td>
<td class="row-actions">
@if ($canManage)
@canPerm('users.assign_role')
<a href="{{ route('admin.users.edit', $u) }}" class="btn btn-sm"><x-icon name="edit" :size="16"/> 角色/权限</a>
@endcanPerm
@canPerm('users.reset_password')
<a href="{{ route('admin.users.edit', $u) }}#reset-password" class="btn btn-sm"><x-icon name="lock" :size="16"/> 重置密码</a>
@endcanPerm
@else
<span class="muted">无权操作</span>
@endif
@canPerm('users.delete')
@if ($canManage && $u->id !== $me->id)
<form method="POST" action="{{ route('admin.users.destroy', $u) }}" onsubmit="return confirm('确认删除该用户?')">@csrf @method('DELETE')<button class="btn btn-sm btn-danger"><x-icon name="trash" :size="16"/></button></form>
@endif
@endcanPerm
</td>
</tr>
@empty
<tr><td colspan="5" class="muted">暂无用户。</td></tr>
@endforelse
</tbody>
</table>
{{ $users->links('pagination::bootstrap-5') }}
@endsection