前端极简黑白重设计,并纳入客户端注册、站点设置与后台相关改动
- storefront 全站改为极简黑白配色:直角、细线分隔、卡片 hover 浅底、深色模式(跟随系统+手动切换) - 抽出 storefront/partials/card 复用卡片 - 一并提交此前暂存的客户端注册 API、站点设置、后台页面与测试 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user