初始化

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,100 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Article;
use App\Models\Category;
use Illuminate\Http\Request;
use Illuminate\View\View;
class ArticleController extends Controller
{
public function index(): View
{
$articles = Article::with('category')->orderByDesc('created_at')->paginate(20);
return view('admin.articles.index', compact('articles'));
}
public function create(): View
{
$categories = Category::ofType('article')->orderBy('sort')->orderBy('name')->get();
return view('admin.articles.form', ['article' => null, 'categories' => $categories]);
}
public function store(Request $request)
{
$data = $request->validate([
'category_id' => ['nullable', 'exists:categories,id'],
'title' => ['required', 'string', 'max:160'],
'slug' => ['required', 'string', 'max:160', 'unique:articles,slug'],
'summary' => ['nullable', 'string', 'max:255'],
'body' => ['nullable', 'string'],
'cover' => ['nullable', 'string', 'max:255'],
'author' => ['nullable', 'string', 'max:60'],
'status' => ['required', 'in:approved,draft'],
'template' => ['required', 'in:standard,tech,feature'],
'seo_title' => ['nullable', 'string', 'max:160'],
'seo_description' => ['nullable', 'string', 'max:255'],
'published_at' => ['nullable', 'date'],
]);
if (! empty($data['body'])) {
$data['body'] = \Purifier::clean($data['body'], 'article');
}
if (empty($data['published_at']) && $data['status'] === 'approved') {
$data['published_at'] = now();
}
Article::create($data);
return redirect()->route('admin.articles.index')->with('success', '文章已创建');
}
public function edit(Article $article): View
{
$categories = Category::ofType('article')->orderBy('sort')->orderBy('name')->get();
return view('admin.articles.form', compact('article', 'categories'));
}
public function update(Request $request, Article $article)
{
$data = $request->validate([
'category_id' => ['nullable', 'exists:categories,id'],
'title' => ['required', 'string', 'max:160'],
'slug' => ['required', 'string', 'max:160', 'unique:articles,slug,' . $article->id],
'summary' => ['nullable', 'string', 'max:255'],
'body' => ['nullable', 'string'],
'cover' => ['nullable', 'string', 'max:255'],
'author' => ['nullable', 'string', 'max:60'],
'status' => ['required', 'in:approved,draft'],
'template' => ['required', 'in:standard,tech,feature'],
'seo_title' => ['nullable', 'string', 'max:160'],
'seo_description' => ['nullable', 'string', 'max:255'],
'published_at' => ['nullable', 'date'],
]);
if (! empty($data['body'])) {
$data['body'] = \Purifier::clean($data['body'], 'article');
}
if (empty($data['published_at']) && $data['status'] === 'approved' && $article->published_at === null) {
$data['published_at'] = now();
}
$article->update($data);
return redirect()->route('admin.articles.index')->with('success', '文章已更新');
}
public function destroy(Article $article)
{
$article->delete();
return redirect()->route('admin.articles.index')->with('success', '文章已删除');
}
}
@@ -0,0 +1,65 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Category;
use Illuminate\Http\Request;
use Illuminate\View\View;
class CategoryController extends Controller
{
public function index(): View
{
$categories = Category::orderBy('type')->orderBy('sort')->orderBy('name')->paginate(20);
return view('admin.categories.index', compact('categories'));
}
public function create(): View
{
return view('admin.categories.form', ['category' => null]);
}
public function store(Request $request)
{
$data = $request->validate([
'name' => ['required', 'string', 'max:60'],
'slug' => ['required', 'string', 'max:80', 'unique:categories,slug'],
'type' => ['required', 'in:product,article'],
'parent_id' => ['nullable', 'integer'],
'sort' => ['integer'],
]);
Category::create($data);
return redirect()->route('admin.categories.index')->with('success', '分类已创建');
}
public function edit(Category $category): View
{
return view('admin.categories.form', compact('category'));
}
public function update(Request $request, Category $category)
{
$data = $request->validate([
'name' => ['required', 'string', 'max:60'],
'slug' => ['required', 'string', 'max:80', 'unique:categories,slug,' . $category->id],
'type' => ['required', 'in:product,article'],
'parent_id' => ['nullable', 'integer'],
'sort' => ['integer'],
]);
$category->update($data);
return redirect()->route('admin.categories.index')->with('success', '分类已更新');
}
public function destroy(Category $category)
{
$category->delete();
return redirect()->route('admin.categories.index')->with('success', '分类已删除');
}
}
@@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\ContactMessage;
use Illuminate\View\View;
class ContactController extends Controller
{
public function index(): View
{
$messages = ContactMessage::orderByDesc('created_at')->paginate(20);
return view('admin.contacts.index', compact('messages'));
}
public function show(ContactMessage $contactMessage): View
{
return view('admin.contacts.show', compact('contactMessage'));
}
public function destroy(ContactMessage $contactMessage)
{
$contactMessage->delete();
return redirect()->route('admin.contacts.index')->with('success', '留言已删除');
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Article;
use App\Models\Category;
use App\Models\ContactMessage;
use App\Models\ForumThread;
use App\Models\Product;
use App\Models\User;
use Illuminate\View\View;
class DashboardController extends Controller
{
public function index(): View
{
$stats = [
'products' => Product::count(),
'articles' => Article::count(),
'categories' => \App\Models\Category::count(),
'glossary' => \App\Models\GlossaryTerm::count(),
'threads' => ForumThread::count(),
'users' => User::count(),
'messages' => ContactMessage::count(),
];
$recentMessages = ContactMessage::orderByDesc('created_at')->limit(5)->get();
$recentThreads = ForumThread::with('user', 'board')->orderByDesc('created_at')->limit(5)->get();
return view('admin.dashboard', compact('stats', 'recentMessages', 'recentThreads'));
}
}
@@ -0,0 +1,53 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\View\View;
/**
* 后台数据库管理:查看迁移状态(已执行 / 待执行),并可一键执行迁移升级。
* 满足「数据库可以随时在后台升级」——代码上线后无需 SSH,点按钮即可 migrate。
*/
class DatabaseController extends Controller
{
public function index(): View
{
$connection = config('database.default');
$dbName = config("database.connections.{$connection}.database");
$ran = [];
if (Schema::hasTable('migrations')) {
$ran = DB::table('migrations')->pluck('migration')->toArray();
}
$files = glob(database_path('migrations/*.php'));
$all = array_map(fn ($f) => basename($f, '.php'), $files);
sort($all);
$done = array_values(array_intersect($all, $ran));
$pending = array_values(array_diff($all, $ran));
return view('admin.database', compact('connection', 'dbName', 'pending', 'done'));
}
public function upgrade(Request $request): RedirectResponse
{
try {
Artisan::call('migrate', ['--force' => true]);
$output = Artisan::output();
return redirect()->route('admin.database')
->with('success', '数据库升级已执行完成。')
->with('migrate_output', $output);
} catch (\Throwable $e) {
return redirect()->route('admin.database')
->with('error', '升级失败:' . $e->getMessage());
}
}
}
@@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\ForumBoard;
use Illuminate\Http\Request;
use Illuminate\View\View;
class ForumBoardController extends Controller
{
public function index(): View
{
$boards = ForumBoard::orderBy('sort')->orderBy('name')->paginate(20);
return view('admin.forum.boards', compact('boards'));
}
public function create(): View
{
return view('admin.forum.board-form', ['board' => null]);
}
public function store(Request $request)
{
$data = $request->validate([
'name' => ['required', 'string', 'max:60'],
'slug' => ['required', 'string', 'max:80', 'unique:forum_boards,slug'],
'description' => ['nullable', 'string'],
'sort' => ['integer'],
]);
ForumBoard::create($data);
return redirect()->route('admin.forum.boards.index')->with('success', '版块已创建');
}
public function edit(ForumBoard $board): View
{
return view('admin.forum.board-form', compact('board'));
}
public function update(Request $request, ForumBoard $board)
{
$data = $request->validate([
'name' => ['required', 'string', 'max:60'],
'slug' => ['required', 'string', 'max:80', 'unique:forum_boards,slug,' . $board->id],
'description' => ['nullable', 'string'],
'sort' => ['integer'],
]);
$board->update($data);
return redirect()->route('admin.forum.boards.index')->with('success', '版块已更新');
}
public function destroy(ForumBoard $board)
{
$board->delete();
return redirect()->route('admin.forum.boards.index')->with('success', '版块已删除');
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\ForumThread;
use Illuminate\View\View;
class ForumThreadController extends Controller
{
public function index(): View
{
$threads = ForumThread::with('user', 'board')
->orderByDesc('created_at')
->paginate(20);
return view('admin.forum.threads', compact('threads'));
}
public function destroy(ForumThread $thread)
{
$thread->delete();
return redirect()->route('admin.forum.threads.index')->with('success', '帖子已删除');
}
}
@@ -0,0 +1,65 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\GlossaryTerm;
use Illuminate\Http\Request;
use Illuminate\View\View;
class GlossaryController extends Controller
{
public function index(): View
{
$terms = GlossaryTerm::orderBy('term')->paginate(20);
return view('admin.glossary.index', compact('terms'));
}
public function create(): View
{
return view('admin.glossary.form', ['term' => null]);
}
public function store(Request $request)
{
$data = $request->validate([
'term' => ['required', 'string', 'max:80'],
'slug' => ['required', 'string', 'max:90', 'unique:glossary_terms,slug'],
'definition' => ['required', 'string'],
'aliases' => ['nullable', 'string', 'max:255'],
'category' => ['nullable', 'string', 'max:60'],
]);
GlossaryTerm::create($data);
return redirect()->route('admin.glossary.index')->with('success', '术语已创建');
}
public function edit(GlossaryTerm $glossaryTerm): View
{
return view('admin.glossary.form', ['term' => $glossaryTerm]);
}
public function update(Request $request, GlossaryTerm $glossaryTerm)
{
$data = $request->validate([
'term' => ['required', 'string', 'max:80'],
'slug' => ['required', 'string', 'max:90', 'unique:glossary_terms,slug,' . $glossaryTerm->id],
'definition' => ['required', 'string'],
'aliases' => ['nullable', 'string', 'max:255'],
'category' => ['nullable', 'string', 'max:60'],
]);
$glossaryTerm->update($data);
return redirect()->route('admin.glossary.index')->with('success', '术语已更新');
}
public function destroy(GlossaryTerm $glossaryTerm)
{
$glossaryTerm->delete();
return redirect()->route('admin.glossary.index')->with('success', '术语已删除');
}
}
@@ -0,0 +1,136 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\HeroSlide;
use App\Models\Setting;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class HeroSlideController extends Controller
{
public function index(): View
{
$slides = HeroSlide::where('location', 'home')->ordered()->get();
$mode = Setting::get('hero_mode', 'carousel');
return view('admin.hero-slides.index', compact('slides', 'mode'));
}
public function create(): View
{
return view('admin.hero-slides.form', ['slide' => null]);
}
public function store(Request $request): RedirectResponse
{
$data = $request->validate([
'title' => ['required', 'string', 'max:120'],
'content' => ['nullable', 'string'],
'cta_text' => ['nullable', 'string', 'max:80'],
'cta_url' => ['nullable', 'string', 'max:255'],
'cta2_text' => ['nullable', 'string', 'max:80'],
'cta2_url' => ['nullable', 'string', 'max:255'],
'is_active' => ['boolean'],
'sort_order' => ['integer'],
]);
$data['location'] = 'home';
$data['is_active'] = $request->boolean('is_active');
$data['sort_order'] = $data['sort_order'] ?? 0;
HeroSlide::create($data);
return redirect()->route('admin.hero-slides.index')->with('success', '轮播项已添加');
}
public function edit(HeroSlide $hero_slide): View
{
return view('admin.hero-slides.form', ['slide' => $hero_slide]);
}
public function update(Request $request, HeroSlide $hero_slide): RedirectResponse
{
$data = $request->validate([
'title' => ['required', 'string', 'max:120'],
'content' => ['nullable', 'string'],
'cta_text' => ['nullable', 'string', 'max:80'],
'cta_url' => ['nullable', 'string', 'max:255'],
'cta2_text' => ['nullable', 'string', 'max:80'],
'cta2_url' => ['nullable', 'string', 'max:255'],
'is_active' => ['boolean'],
'sort_order' => ['integer'],
]);
$data['is_active'] = $request->boolean('is_active');
$data['sort_order'] = $data['sort_order'] ?? $hero_slide->sort_order;
$hero_slide->update($data);
return redirect()->route('admin.hero-slides.index')->with('success', '轮播项已更新');
}
public function destroy(HeroSlide $hero_slide): RedirectResponse
{
$hero_slide->delete();
return redirect()->route('admin.hero-slides.index')->with('success', '轮播项已删除');
}
/**
* 上移 / 下移排序。
*/
public function move(Request $request, HeroSlide $hero_slide): RedirectResponse
{
$direction = $request->input('direction');
if ($direction === 'up') {
$sibling = HeroSlide::where('location', 'home')->ordered()
->where('sort_order', '<', $hero_slide->sort_order)
->orderByDesc('sort_order')
->first();
if ($sibling) {
[$hero_slide->sort_order, $sibling->sort_order] = [$sibling->sort_order, $hero_slide->sort_order];
$hero_slide->save();
$sibling->save();
}
} elseif ($direction === 'down') {
$sibling = HeroSlide::where('location', 'home')->ordered()
->where('sort_order', '>', $hero_slide->sort_order)
->first();
if ($sibling) {
[$hero_slide->sort_order, $sibling->sort_order] = [$sibling->sort_order, $hero_slide->sort_order];
$hero_slide->save();
$sibling->save();
}
}
return redirect()->route('admin.hero-slides.index');
}
/**
* 一键启用 / 停用。
*/
public function toggle(HeroSlide $hero_slide): RedirectResponse
{
$hero_slide->update(['is_active' => ! $hero_slide->is_active]);
return redirect()->route('admin.hero-slides.index');
}
/**
* 全局 Hero 显示模式:carousel(轮播)/ single(独立单图)。
*/
public function setMode(Request $request): RedirectResponse
{
$data = $request->validate([
'hero_mode' => ['required', 'in:carousel,single'],
]);
Setting::set('hero_mode', $data['hero_mode'], 'appearance');
return redirect()->route('admin.hero-slides.index')->with('success', 'Hero 显示模式已更新');
}
}
@@ -0,0 +1,111 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\NavigationMenu;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class MenuController extends Controller
{
public function index(): View
{
$menus = NavigationMenu::ordered()->get();
return view('admin.menus.index', compact('menus'));
}
public function create(): View
{
return view('admin.menus.form', ['menu' => null]);
}
public function store(Request $request): RedirectResponse
{
$data = $request->validate([
'label' => ['required', 'string', 'max:60'],
'url' => ['required', 'string', 'max:255'],
'is_visible' => ['boolean'],
'sort_order' => ['integer'],
]);
$data['is_visible'] = $request->boolean('is_visible');
$data['sort_order'] = $data['sort_order'] ?? 0;
NavigationMenu::create($data);
return redirect()->route('admin.menus.index')->with('success', '菜单项已添加');
}
public function edit(NavigationMenu $menu): View
{
return view('admin.menus.form', compact('menu'));
}
public function update(Request $request, NavigationMenu $menu): RedirectResponse
{
$data = $request->validate([
'label' => ['required', 'string', 'max:60'],
'url' => ['required', 'string', 'max:255'],
'is_visible' => ['boolean'],
'sort_order' => ['integer'],
]);
$data['is_visible'] = $request->boolean('is_visible');
$data['sort_order'] = $data['sort_order'] ?? $menu->sort_order;
$menu->update($data);
return redirect()->route('admin.menus.index')->with('success', '菜单项已更新');
}
public function destroy(NavigationMenu $menu): RedirectResponse
{
$menu->delete();
return redirect()->route('admin.menus.index')->with('success', '菜单项已删除');
}
/**
* 上移 / 下移排序。
*/
public function move(Request $request, NavigationMenu $menu): RedirectResponse
{
$direction = $request->input('direction');
if ($direction === 'up') {
$sibling = NavigationMenu::ordered()
->where('sort_order', '<', $menu->sort_order)
->orderByDesc('sort_order')
->first();
if ($sibling) {
[$menu->sort_order, $sibling->sort_order] = [$sibling->sort_order, $menu->sort_order];
$menu->save();
$sibling->save();
}
} elseif ($direction === 'down') {
$sibling = NavigationMenu::ordered()
->where('sort_order', '>', $menu->sort_order)
->first();
if ($sibling) {
[$menu->sort_order, $sibling->sort_order] = [$sibling->sort_order, $menu->sort_order];
$menu->save();
$sibling->save();
}
}
return redirect()->route('admin.menus.index');
}
/**
* 一键显示 / 隐藏。
*/
public function toggle(NavigationMenu $menu): RedirectResponse
{
$menu->update(['is_visible' => ! $menu->is_visible]);
return redirect()->route('admin.menus.index');
}
}
@@ -0,0 +1,95 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Article;
use App\Models\ForumReply;
use App\Models\ForumThread;
use Illuminate\Http\Request;
/**
* 统一审核队列:文章投稿 / 论坛帖子 / 论坛回复 的待审内容。
* 入口按权限展示对应 Tabarticles.moderate / forum.moderate)。
*/
class ModerationController extends Controller
{
public function index(Request $request)
{
$user = $request->user();
$tab = $request->query('tab', 'articles');
$canArticles = $user->canPerm('articles.moderate');
$canForum = $user->canPerm('forum.moderate');
if (! $canArticles && ! $canForum) {
abort(403, '无审核权限');
}
if ($tab === 'threads' && ! $canForum) {
$tab = $canArticles ? 'articles' : 'replies';
}
if ($tab === 'replies' && ! $canForum) {
$tab = $canArticles ? 'articles' : 'threads';
}
$articles = $canArticles
? Article::where('status', 'pending')->with('user', 'category')->orderByDesc('created_at')->paginate(10, ['*'], 'ap')
: null;
$threads = $canForum
? ForumThread::where('status', 'pending')->with('user', 'board')->orderByDesc('created_at')->paginate(10, ['*'], 'tp')
: null;
$replies = $canForum
? ForumReply::where('status', 'pending')->with('user', 'thread')->orderByDesc('created_at')->paginate(10, ['*'], 'rp')
: null;
return view('admin.moderation.index', compact('tab', 'articles', 'threads', 'replies', 'canArticles', 'canForum'));
}
public function approveArticle(Article $article)
{
$article->update([
'status' => 'approved',
'published_at' => $article->published_at ?? now(),
]);
return back()->with('success', "已通过文章《{$article->title}");
}
public function rejectArticle(Article $article)
{
$article->update(['status' => 'rejected']);
return back()->with('success', "已驳回文章《{$article->title}");
}
public function approveThread(ForumThread $thread)
{
$thread->update(['status' => ForumThread::STATUS_APPROVED]);
return back()->with('success', "已通过帖子《{$thread->title}");
}
public function rejectThread(ForumThread $thread)
{
$thread->update(['status' => ForumThread::STATUS_REJECTED]);
return back()->with('success', "已驳回帖子《{$thread->title}");
}
public function approveReply(ForumReply $reply)
{
$reply->update(['status' => ForumReply::STATUS_APPROVED]);
return back()->with('success', '已通过该回复');
}
public function rejectReply(ForumReply $reply)
{
$reply->update(['status' => ForumReply::STATUS_REJECTED]);
return back()->with('success', '已驳回该回复');
}
}
@@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\News;
use Illuminate\Http\Request;
use Illuminate\View\View;
class NewsController extends Controller
{
public function index(): View
{
$news = News::orderByDesc('published_at')->paginate(20);
return view('admin.news.index', compact('news'));
}
public function create(): View
{
return view('admin.news.form', ['news' => null]);
}
public function store(Request $request)
{
$data = $request->validate([
'title' => ['required', 'string', 'max:160'],
'slug' => ['required', 'string', 'max:160', 'unique:news,slug'],
'category' => ['required', 'in:industry,company,event'],
'summary' => ['nullable', 'string', 'max:255'],
'body' => ['nullable', 'string'],
'cover' => ['nullable', 'string', 'max:255'],
'status' => ['required', 'in:published,draft'],
'published_at' => ['nullable', 'date'],
]);
$data['published_at'] = $data['published_at'] ?? now();
News::create($data);
return redirect()->route('admin.news.index')->with('success', '新闻已创建');
}
public function edit(News $news): View
{
return view('admin.news.form', compact('news'));
}
public function update(Request $request, News $news)
{
$data = $request->validate([
'title' => ['required', 'string', 'max:160'],
'slug' => ['required', 'string', 'max:160', 'unique:news,slug,' . $news->id],
'category' => ['required', 'in:industry,company,event'],
'summary' => ['nullable', 'string', 'max:255'],
'body' => ['nullable', 'string'],
'cover' => ['nullable', 'string', 'max:255'],
'status' => ['required', 'in:published,draft'],
'published_at' => ['nullable', 'date'],
]);
$news->update($data);
return redirect()->route('admin.news.index')->with('success', '新闻已更新');
}
public function destroy(News $news)
{
$news->delete();
return redirect()->route('admin.news.index')->with('success', '新闻已删除');
}
}
@@ -0,0 +1,80 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Category;
use App\Models\Product;
use Illuminate\Http\Request;
use Illuminate\View\View;
class ProductController extends Controller
{
public function index(): View
{
$products = Product::with('category')->orderBy('sort')->orderBy('name')->paginate(20);
return view('admin.products.index', compact('products'));
}
public function create(): View
{
$categories = Category::ofType('product')->orderBy('sort')->orderBy('name')->get();
return view('admin.products.form', ['product' => null, 'categories' => $categories]);
}
public function store(Request $request)
{
$data = $request->validate([
'category_id' => ['nullable', 'exists:categories,id'],
'name' => ['required', 'string', 'max:120'],
'slug' => ['required', 'string', 'max:120', 'unique:products,slug'],
'model' => ['nullable', 'string', 'max:60'],
'summary' => ['nullable', 'string', 'max:255'],
'description' => ['nullable', 'string'],
'specs' => ['nullable', 'string'],
'cover' => ['nullable', 'string', 'max:255'],
'status' => ['required', 'in:published,draft'],
'sort' => ['integer'],
]);
Product::create($data);
return redirect()->route('admin.products.index')->with('success', '产品已创建');
}
public function edit(Product $product): View
{
$categories = Category::ofType('product')->orderBy('sort')->orderBy('name')->get();
return view('admin.products.form', compact('product', 'categories'));
}
public function update(Request $request, Product $product)
{
$data = $request->validate([
'category_id' => ['nullable', 'exists:categories,id'],
'name' => ['required', 'string', 'max:120'],
'slug' => ['required', 'string', 'max:120', 'unique:products,slug,' . $product->id],
'model' => ['nullable', 'string', 'max:60'],
'summary' => ['nullable', 'string', 'max:255'],
'description' => ['nullable', 'string'],
'specs' => ['nullable', 'string'],
'cover' => ['nullable', 'string', 'max:255'],
'status' => ['required', 'in:published,draft'],
'sort' => ['integer'],
]);
$product->update($data);
return redirect()->route('admin.products.index')->with('success', '产品已更新');
}
public function destroy(Product $product)
{
$product->delete();
return redirect()->route('admin.products.index')->with('success', '产品已删除');
}
}
@@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Setting;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class SettingsController extends Controller
{
public function index(): View
{
$settings = Setting::pluck('value', 'key')->toArray();
return view('admin.settings', compact('settings'));
}
public function update(Request $request): RedirectResponse
{
$data = $request->validate([
'site_name' => ['required', 'string', 'max:80'],
'site_description' => ['nullable', 'string', 'max:255'],
'seo_title_default' => ['nullable', 'string', 'max:160'],
'seo_description_default' => ['nullable', 'string', 'max:255'],
'icp_no' => ['nullable', 'string', 'max:60'],
'security_filing_no' => ['nullable', 'string', 'max:60'],
'logo_url' => ['nullable', 'string', 'max:255'],
'favicon_url' => ['nullable', 'string', 'max:255'],
'contact_email' => ['nullable', 'email', 'max:120'],
'theme' => ['nullable', 'string', 'in:azure,slate,indigo,teal,amber'],
'footer_content' => ['nullable', 'string', 'max:20000'],
]);
$seoKeys = ['site_description', 'seo_title_default', 'seo_description_default'];
foreach ($data as $key => $value) {
$group = in_array($key, $seoKeys, true) ? 'seo' : 'general';
Setting::set($key, $value ?: null, $group);
}
return redirect()->route('admin.settings')->with('success', '站点设置已保存');
}
}
@@ -0,0 +1,79 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Solution;
use Illuminate\Http\Request;
use Illuminate\View\View;
class SolutionController extends Controller
{
public function index(): View
{
$solutions = Solution::orderBy('sort')->orderBy('name')->paginate(20);
return view('admin.solutions.index', compact('solutions'));
}
public function create(): View
{
return view('admin.solutions.form', ['solution' => null]);
}
public function store(Request $request)
{
$data = $request->validate([
'name' => ['required', 'string', 'max:120'],
'slug' => ['required', 'string', 'max:120', 'unique:solutions,slug'],
'power_segment' => ['nullable', 'in:lt30,30-65,65-100,gt100'],
'application' => ['nullable', 'in:charger,powerbank,car,dock'],
'chip_platform' => ['nullable', 'in:protocol,soc,rectifier'],
'power' => ['nullable', 'string', 'max:40'],
'summary' => ['nullable', 'string', 'max:255'],
'advantages' => ['nullable', 'string'],
'description' => ['nullable', 'string'],
'cover' => ['nullable', 'string', 'max:255'],
'status' => ['required', 'in:published,draft'],
'sort' => ['integer'],
]);
Solution::create($data);
return redirect()->route('admin.solutions.index')->with('success', '方案已创建');
}
public function edit(Solution $solution): View
{
return view('admin.solutions.form', compact('solution'));
}
public function update(Request $request, Solution $solution)
{
$data = $request->validate([
'name' => ['required', 'string', 'max:120'],
'slug' => ['required', 'string', 'max:120', 'unique:solutions,slug,' . $solution->id],
'power_segment' => ['nullable', 'in:lt30,30-65,65-100,gt100'],
'application' => ['nullable', 'in:charger,powerbank,car,dock'],
'chip_platform' => ['nullable', 'in:protocol,soc,rectifier'],
'power' => ['nullable', 'string', 'max:40'],
'summary' => ['nullable', 'string', 'max:255'],
'advantages' => ['nullable', 'string'],
'description' => ['nullable', 'string'],
'cover' => ['nullable', 'string', 'max:255'],
'status' => ['required', 'in:published,draft'],
'sort' => ['integer'],
]);
$solution->update($data);
return redirect()->route('admin.solutions.index')->with('success', '方案已更新');
}
public function destroy(Solution $solution)
{
$solution->delete();
return redirect()->route('admin.solutions.index')->with('success', '方案已删除');
}
}
@@ -0,0 +1,228 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\View\View;
class UserController extends Controller
{
public function index(Request $request)
{
$q = trim((string) $request->query('q', ''));
$users = User::query()
->when($q, fn ($query) => $query->where(function ($sub) use ($q) {
$sub->where('name', 'like', "%{$q}%")->orWhere('email', 'like', "%{$q}%");
}))
->orderByDesc('created_at')
->paginate(20)
->withQueryString();
return view('admin.users.index', compact('users', 'q'));
}
/**
* 新建用户表单(角色 + 权限分配)。
*/
public function create(): View
{
$roles = config('permissions.roles');
$groups = config('permissions.groups');
$permissions = config('permissions.permissions');
return view('admin.users.create', compact('roles', 'groups', 'permissions'));
}
/**
* 新建用户,受层级约束:不能创建权限高于或等于自身角色的用户。
*/
public function store(Request $request): RedirectResponse
{
$current = $request->user();
$allPermKeys = implode(',', array_keys(config('permissions.permissions')));
$data = $request->validate([
'name' => ['required', 'string', 'max:60'],
'email' => ['required', 'email', 'unique:users,email'],
'password' => ['required', 'string', 'min:8'],
'role' => ['required', 'in:super_admin,admin,forum_admin,staff,user'],
'permissions' => ['nullable', 'array'],
'permissions.*' => ['string', "in:{$allPermKeys}"],
]);
// 层级防护:不能创建高于或等于自身权限的角色
if (! $current->isSuperAdmin() && $current->roleLevel() <= $this->roleLevelOf($data['role'])) {
return back()->withErrors(['role' => '您只能创建权限低于自身的用户'])->withInput();
}
if ($data['role'] === 'super_admin' && ! $current->isSuperAdmin()) {
return back()->withErrors(['role' => '只有超级管理员可以创建超级管理员'])->withInput();
}
$permissions = $data['role'] === 'staff'
? array_values(array_unique($data['permissions'] ?? []))
: null;
$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
'role' => $data['role'],
'is_admin' => $data['role'] !== 'user',
'permissions' => $permissions,
'points' => 0,
]);
return redirect()->route('admin.users.index')
->with('success', "已创建用户 {$user->name}");
}
/**
* 编辑用户角色与权限(内部员工可勾选细粒度权限)。
*/
public function edit(User $user)
{
$roles = config('permissions.roles');
$groups = config('permissions.groups');
$permissions = config('permissions.permissions');
$userPerms = is_array($user->permissions) ? $user->permissions : [];
$canManage = $this->canManage($user);
return view('admin.users.edit', compact('user', 'roles', 'groups', 'permissions', 'userPerms', 'canManage'));
}
/**
* 更新用户角色与权限,受层级约束。
*/
public function update(Request $request, User $user): RedirectResponse
{
$current = $request->user();
// 防锁死:禁止改动自己
if ($user->id === $current->id) {
return back()->withErrors(['role' => '不能修改当前登录账号的角色/权限']);
}
// 层级防护:非超管不能触碰同级或更高权限用户
if (! $current->isSuperAdmin() && $current->roleLevel() <= $user->roleLevel()) {
return back()->withErrors(['role' => '您只能修改权限低于自身的用户']);
}
if ($user->isSuperAdmin() && ! $current->isSuperAdmin()) {
return back()->withErrors(['role' => '只有超级管理员可以调整超级管理员']);
}
$allPermKeys = implode(',', array_keys(config('permissions.permissions')));
$data = $request->validate([
'role' => ['required', 'in:super_admin,admin,forum_admin,staff,user'],
'permissions' => ['nullable', 'array'],
'permissions.*' => ['string', "in:{$allPermKeys}"],
]);
// 越权防护:不能分配高于或等于自身权限的角色
if ($data['role'] === 'super_admin' && ! $current->isSuperAdmin()) {
return back()->withErrors(['role' => '只有超级管理员可以分配超级管理员角色']);
}
if (! $current->isSuperAdmin() && $current->roleLevel() <= $this->roleLevelOf($data['role'])) {
return back()->withErrors(['role' => '您只能分配权限低于自身的角色']);
}
$permissions = $data['role'] === 'staff'
? array_values(array_unique($data['permissions'] ?? []))
: null;
$user->update([
'role' => $data['role'],
'is_admin' => $data['role'] !== 'user',
'permissions' => $permissions,
]);
return redirect()->route('admin.users.index')
->with('success', "已更新 {$user->name} 的角色与权限");
}
/**
* 重置指定用户密码(管理员代操作):可指定新密码,留空则生成随机密码并回显。
* 受层级约束,且禁止重置当前登录账号(请用「修改密码」)。
*/
public function resetPassword(Request $request, User $user): RedirectResponse
{
$current = $request->user();
if ($user->id === $current->id) {
return back()->withErrors(['password' => '不能重置当前登录账号的密码,请到「修改密码」操作']);
}
if (! $current->isSuperAdmin() && $current->roleLevel() <= $user->roleLevel()) {
return back()->withErrors(['password' => '您只能重置权限低于自身的用户密码']);
}
if ($user->isSuperAdmin() && ! $current->isSuperAdmin()) {
return back()->withErrors(['password' => '只有超级管理员可以重置超级管理员密码']);
}
$data = $request->validate([
'password' => ['nullable', 'string', 'min:8', 'confirmed'],
]);
if (! empty($data['password'])) {
$new = $data['password'];
$msg = '密码已重置';
} else {
$new = $this->generatePassword(12);
$msg = '密码已重置为随机密码:' . $new . '(请妥善告知用户,并建议其尽快修改)';
}
$user->update(['password' => Hash::make($new)]);
return redirect()->route('admin.users.edit', $user)->with('success', $msg);
}
public function destroy(User $user): RedirectResponse
{
$current = request()->user();
if ($user->id === $current->id) {
return back()->withErrors(['name' => '不能删除当前登录账号']);
}
if (! $current->isSuperAdmin() && $current->roleLevel() <= $user->roleLevel()) {
return back()->withErrors(['name' => '您只能删除权限低于自身的用户']);
}
if ($user->isSuperAdmin() && ! $current->isSuperAdmin()) {
return back()->withErrors(['name' => '只有超级管理员可以删除超级管理员']);
}
$user->delete();
return redirect()->route('admin.users.index')->with('success', '用户已删除');
}
/**
* 当前用户是否可管理目标用户(用于视图显隐操作按钮)。
*/
protected function canManage(User $user): bool
{
$current = request()->user();
if ($current->isSuperAdmin()) {
return true;
}
return $current->roleLevel() > $user->roleLevel();
}
protected function roleLevelOf(string $role): int
{
return (int) config("permissions.roles.{$role}.level", 1);
}
protected function generatePassword(int $len): string
{
$pool = 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
$out = '';
for ($i = 0; $i < $len; $i++) {
$out .= $pool[random_int(0, strlen($pool) - 1)];
}
return $out;
}
}