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

- 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 -6
View File
@@ -7,6 +7,8 @@ use App\Models\Package;
use App\Models\Version;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
class AdminPackageService
{
@@ -114,6 +116,19 @@ class AdminPackageService
});
}
public function deletePackage(Package $package): void
{
$storageDir = storage_path('app/packages/' . $package->type . '/' . $package->slug);
DB::transaction(function () use ($package) {
$package->delete();
});
if (is_dir($storageDir)) {
File::deleteDirectory($storageDir);
}
}
public function refreshLatestVersion(Package $package): void
{
$latestStable = $package->versions()
@@ -141,19 +156,88 @@ class AdminPackageService
private function syncCategories(Package $package, array $categorySlugs): void
{
$categorySlugs = array_values(array_unique(array_filter(array_map('strval', $categorySlugs))));
$categorySlugs = collect($categorySlugs)
->map(fn ($item) => trim((string) $item))
->filter()
->unique(fn (string $item) => mb_strtolower($item, 'UTF-8'))
->values()
->all();
if (empty($categorySlugs)) {
$package->categories()->sync([]);
return;
}
$categoryIds = Category::query()
$categories = Category::query()
->where('type', $package->type)
->whereIn('slug', $categorySlugs)
->pluck('id')
->all();
->get(['id', 'type', 'slug', 'name']);
$package->categories()->sync($categoryIds);
$categoryIds = [];
foreach ($categorySlugs as $token) {
$normalizedToken = mb_strtolower($token, 'UTF-8');
$existing = $categories->first(function (Category $category) use ($token, $normalizedToken) {
return $category->name === $token
|| mb_strtolower($category->slug, 'UTF-8') === $normalizedToken
|| mb_strtolower($category->name, 'UTF-8') === $normalizedToken;
});
if ($existing) {
$categoryIds[] = $existing->id;
continue;
}
$slug = $this->generateCategorySlug($token, $categories);
$category = Category::query()->create([
'type' => $package->type,
'slug' => $slug,
'name' => $this->generateCategoryName($token),
'description' => '',
'sort_order' => 0,
]);
$categories->push($category);
$categoryIds[] = $category->id;
}
$package->categories()->sync(array_values(array_unique($categoryIds)));
}
private function generateCategorySlug(string $token, $categories): string
{
$base = (string) Str::of($token)
->trim()
->replace('_', '-')
->replaceMatches('/\s+/u', '-')
->replaceMatches('/[^\pL\pN-]+/u', '-')
->replaceMatches('/-+/u', '-')
->lower()
->trim('-');
if ($base === '') {
$base = 'category';
}
$slug = $base;
$suffix = 2;
while ($categories->contains(fn (Category $category) => mb_strtolower($category->slug, 'UTF-8') === mb_strtolower($slug, 'UTF-8'))) {
$slug = $base . '-' . $suffix;
$suffix++;
}
return $slug;
}
private function generateCategoryName(string $token): string
{
if (preg_match('/^[a-z0-9_-]+$/i', $token) === 1) {
return (string) Str::of($token)
->replace(['-', '_'], ' ')
->title();
}
return trim($token);
}
}
+85
View File
@@ -0,0 +1,85 @@
<?php
namespace App\Services;
use App\Models\StoreSetting;
use Illuminate\Support\Facades\Schema;
use RuntimeException;
use Throwable;
class StoreSettings
{
private ?array $resolved = null;
public function all(): array
{
if ($this->resolved !== null) {
return $this->resolved;
}
$defaults = $this->defaults();
if (!$this->tableExists()) {
return $this->resolved = $defaults;
}
$stored = StoreSetting::query()
->pluck('value', 'key')
->map(fn ($value) => (string) $value)
->all();
return $this->resolved = array_replace($defaults, $stored);
}
public function get(string $key, mixed $default = null): mixed
{
$settings = $this->all();
return $settings[$key] ?? $default;
}
public function defaults(): array
{
return (array) config('store.settings_defaults', []);
}
public function keys(): array
{
return array_keys($this->defaults());
}
public function setMany(array $values): void
{
if (!$this->tableExists()) {
throw new RuntimeException('store_settings table is missing. Please run php artisan migrate.');
}
$now = now();
$rows = collect($values)
->only($this->keys())
->map(fn ($value, $key) => [
'key' => $key,
'value' => (string) $value,
'created_at' => $now,
'updated_at' => $now,
])
->values()
->all();
if ($rows === []) {
return;
}
StoreSetting::query()->upsert($rows, ['key'], ['value', 'updated_at']);
$this->resolved = null;
}
private function tableExists(): bool
{
try {
return Schema::hasTable('store_settings');
} catch (Throwable) {
return false;
}
}
}
+6 -1
View File
@@ -155,7 +155,7 @@ class VersionPublishService
throw new RuntimeException('manifest slug does not match package slug');
}
if (!preg_match('/^[A-Za-z][A-Za-z0-9]*$/', (string) ($manifest['slug'] ?? ''))) {
if (!$this->isValidSlug((string) ($manifest['slug'] ?? ''))) {
throw new RuntimeException('manifest slug format invalid');
}
@@ -213,6 +213,11 @@ class VersionPublishService
return $base === '' ? $path : $base . $path;
}
private function isValidSlug(string $slug): bool
{
return preg_match('/^[A-Za-z][A-Za-z0-9_-]{0,63}$/', $slug) === 1;
}
private function assertTopLevelStructure(array $topLevel, array $allowedTopLevel, string $message): void
{
$topLevelNames = array_keys($topLevel);