feat: restore sablog browse modes and clean imported HTML
Build and Deploy / verify (push) Successful in 40s
Build and Deploy / deploy (push) Successful in 36s

Keep the public site readable after import (nbsp, highlighter, wrapping code, compact pager) and finish the admin extras already in this tree: sticky levels, preview, page cache, and live llms.txt.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
gouki
2026-09-18 21:33:02 +00:00
co-authored by Cursor
parent 469160dc3f
commit ae47447062
98 changed files with 8618 additions and 901 deletions
+20
View File
@@ -1,5 +1,25 @@
# Changelog
## 2026-09-18
- Homepage / lists have SaBlog-style **标准 | 列表** browse modes (`home_view_mode`, `?view=`, cookie).
- Pagination is compact (`第 x / y 页` + fewer page chips) and the content column uses CSS grid so it no longer sits under the sidebar.
- Imported SaBlog HTML no longer shows literal `&nbsp;`; highlighter / stacked docker `<div>` blocks become wrapped `<pre><code>`.
- `php artisan sablog:fix-content --all` can persist that cleanup. Code no longer stretches the page.
- Schema snapshot: `docs/database/mysql-schema-20260918-211824.sql`.
- Frontend RSS / Sitemap / llms.txt links now open in a new tab.
- `/llms.txt` is a core live route (plus `/posts/{slug}.md`) so agents can crawl new public posts without a static file.
- Fixed edit-post crash: comments relation manager now uses Filament v5 `Filament\Actions\Action`.
- Posts now have sticky levels 0–3 (none / light / strong / headline). Headline always stays at the top of public lists.
- Edit and list screens have a **预览** button; admin/editor can open unpublished posts on the public site.
- Admin top bar **网站** opens the frontend in a new tab. Sidebar **管理** now has **页面缓存** and **语言**.
- Schema snapshot: `docs/database/mysql-schema-20260918-205818.sql`.
- Post create/edit form is now left content 2 : right tools 1, locked for PC at every breakpoint (not only `lg`).
- Slug is full width; category, format, and status share one row as single-choice buttons; cover sits above the taller body editor.
- Right column: publish settings, then access settings (password + membership price fields).
- Cover / media picker preview is a small thumbnail (max 220×124), not full column width.
- Schema snapshot: `docs/database/mysql-schema-20260918-203827.sql`.
## 2026-09-17
- Admin top-right now shows the current environment (`local` / `prod`) and a **写文章** shortcut to create a post.
+43 -1
View File
@@ -12,10 +12,31 @@ Modern blog migrated from SaBlog-X: Filament admin, themes, plugins, sablog impo
See `README.md` and `docs/` for install, import, plugins, and themes.
## Post editor layout
- Create/edit post uses **left 2 : right 1** on PC at every breakpoint (not only `lg`).
- Left: title, full-width slug, category + format + status on one row (single-choice buttons), cover thumbnail (not full-width), then a tall body editor.
- Right group 1: published at, sticky level (none / light / strong / headline), comment status, excerpt, keywords.
- Right group 2: read password; membership plugin adds members-only + price here.
- Plugins must inject via `PostForm::appendToSidebar()` / `appendToAccessSection()` so they do not break the 2:1 grid.
## Sticky levels
- `sticky_level`: 0 none, 1 light, 2 strong, 3 headline. Headline always stays first on home, category, tag, and search lists.
- Saving a post keeps `is_sticky` in sync (`true` when level > 0) so older themes still work.
- List badges show the Chinese label, not a generic “sticky”.
## Post preview
- Edit page and post table have **预览**, opening `/show-{id}.shtml` in a new tab.
- Filament v5 actions are `Filament\Actions\Action`. The comments relation manager on the edit page must boot in tests, because a stale `Tables\Actions\Action` import only fails when that Livewire table renders.
- Guests still get 404 for drafts. Admin and editor can open unpublished posts and see a preview banner.
## Admin chrome
- Top-right badge shows the current app environment (`local` / `prod` / others).
- Top-right **写文章** button jumps to `/admin/posts/create`.
- Top-right **网站** opens the public site in a new tab; **写文章** jumps to `/admin/posts/create`.
- Sidebar **管理** includes **页面缓存** (view URLs, hit rate, flush) and **语言** (current locale + available packs). Article translations are not built yet; the language page says so.
- Sidebar nav is compacted (Filament default `gap-y-7` / `py-8` is too tall for this site).
## Hello World plugin
@@ -24,6 +45,27 @@ See `README.md` and `docs/` for install, import, plugins, and themes.
- Plugin boot / hook failures are isolated so one plugin cannot take down the whole admin.
- `/hello-world` stays available even if `hello_world_logs` is missing.
## Frontend feeds
- Header/footer **RSS / Sitemap / llms.txt** open in a new tab (`target="_blank"`).
- `/llms.txt` is a core route (not a static file). Each request lists current published public posts so new articles show up immediately.
- Full text for agents: `/posts/{slug}.md`. Password / members-only posts are omitted.
- These machine-readable paths are excluded from page cache.
## Homepage list / full mode
- Same switch as old SaBlog: **浏览模式: 标准 | 列表**.
- `list` = title + excerpt cards. `full` = full rendered body on the home / category / tag / search lists.
- Query `?view=list` or `?view=full` wins, then cookie `ll_home_view`, then setting `home_view_mode`.
- Page cache key includes the view mode so the two layouts are not mixed.
## Pagination and imported HTML
- Public pagers use `onEachSide(1)` plus “第 x / y 页”, and the main column is CSS grid `minmax(0, 1fr)` so page chips stay in the content column.
- `SablogHtmlNormalizer` strips leftover `&nbsp;`, turns SyntaxHighlighter `codeText` / `dp-*` lists into `<pre><code>`, and wraps stacked docker/shell `<div>` lines.
- Render path always normalizes, so already-imported posts look right without rewriting the database. Persist with `php artisan sablog:fix-content --all`.
- Code blocks wrap / scroll inside the column instead of stretching the page.
## Media library (xpier)
- Package: `xpier/filament-media-library` (admin Media Library + MediaPicker).
+8 -3
View File
@@ -5,8 +5,11 @@
- **脚本迁移**:`php artisan sablog:import` 一键从老库迁入全部数据(文章/分类/评论/标签/友链/用户/附件),保留原始 ID,老 URL 全部 301 兼容
- **多主题**:`themes/` 目录即皮肤,后台一键切换/上传 ZIP;内置 sablog 经典风 + modern 简约风两套
- **插件系统**:`plugins/` 目录 + 钩子系统(action/filter),ZIP 安装 + 可选远程市场;启用时自动跑插件迁移
- **后台顶栏**:右上角显示当前环境(local/prod)+ 一键写文章
- **内置插件**:SEO & GEO(JSON-LD/OG/llms.txt/Markdown 导出)、AI 评论审核 + 内容润色(Workerman 异步)、支付(支付宝/微信/沙箱)、会员(套餐订阅 + 付费文章)
- **后台顶栏**:右上角显示当前环境(local/prod)+ 打开网站 + 一键写文章
- **文章编辑**:左正文 2 : 右功能 1(Slug 整行,分类/格式/状态同一行,封面在正文上方)
- **内置插件**:SEO & GEO(JSON-LD/OG)、AI 评论审核 + 内容润色(Workerman 异步)、支付(支付宝/微信/沙箱)、会员(套餐订阅 + 付费文章)
- **首页浏览模式**:和老 sablog 一样可切 **标准(全文) / 列表(摘要)**
- **Agent 索引**:`/llms.txt`、`/posts/{slug}.md` 是核心路由,随已发布公开文章实时生成
- **附件全走 S3**:R2 / COS / OSS 兼容,新上传零落盘,`attachments:sync-s3` 一键同步老附件(大文件 Multipart)
## 技术栈
@@ -59,6 +62,8 @@ php artisan sablog:import ... --convert-markdown
# 事后单独转换 / 同步附件到 S3
php artisan content:convert --all
# 已导入正文里的 &nbsp; / 高亮块 / docker 分行,可先渲染时自动清洗,也可落库:
php artisan sablog:fix-content --all
S3_ACCESS_KEY=... S3_BUCKET=... S3_ENDPOINT=... php artisan attachments:sync-s3 --legacy-dir=/path/to/old/attachments
```
@@ -118,7 +123,7 @@ php artisan workerman:serve stop
- 匿名 GET 页面整页缓存(首页/列表/文章/归档/标签等),TTL 默认 300 秒(`PAGE_CACHE_TTL`),命中时零数据库查询
- 文章浏览量由独立 `/track-view` 端点统计,不受整页缓存影响(实时)
- 后台「缓存管理」:清空页面/侧边栏/设置/全部缓存、重建编译缓存
- 后台「管理 → 页面缓存」:查看已缓存地址、命中率,清空前台或全部缓存
- 侧边栏数据与站点设置另有 1 小时缓存
## 对外 API
+7
View File
@@ -0,0 +1,7 @@
# Reminders
## Filament v5 actions
- Table / header / record actions use `Filament\Actions\Action`.
- `Filament\Tables\Actions\Action` does not exist. A `use` line only blows up when the class is actually called (for example the comments relation manager on the edit-post page).
- After changing Filament pages, boot the Livewire component, not only `GET` the HTML.
+6 -5
View File
@@ -34,11 +34,12 @@ class ArchiveController extends Controller
$start = Carbon::create((int) $year, (int) $month, 1)->startOfMonth();
$end = (clone $start)->endOfMonth();
$posts = Post::query()
->whereBetween('published_at', [$start, $end])
->published()
->orderByDesc('published_at')
->paginate((int) blog_setting('per_page', config('blog.per_page')));
$posts = blog_paginate(
Post::query()
->whereBetween('published_at', [$start, $end])
->published()
->orderByDesc('published_at')
);
return theme_view('list', compact('posts'))
->with('archiveTitle', "{$year} 年 {$month} 月");
+3 -6
View File
@@ -2,11 +2,9 @@
declare(strict_types=1);
namespace App\Blog\Controllers;
use App\Models\Category;
use App\Models\Post;
class CategoryController extends Controller
{
@@ -14,10 +12,9 @@ class CategoryController extends Controller
{
$category = Category::query()->where('slug', $slug)->orWhere('id', (int) $slug)->firstOrFail();
$posts = $category->posts()
->published()
->latest('published_at')
->paginate((int) blog_setting('per_page', config('blog.per_page')));
$posts = blog_paginate(
$category->posts()->published()->orderedForList()
);
return theme_view('list', compact('category', 'posts'));
}
+3 -3
View File
@@ -2,12 +2,10 @@
declare(strict_types=1);
namespace App\Blog\Controllers;
use App\Models\Post;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\DB;
class FeedController extends Controller
{
@@ -54,7 +52,9 @@ class FeedController extends Controller
foreach ($disallow as $path) {
$lines[] = "Disallow: {$path}";
}
$lines[] = "Sitemap: ".url($sitemap);
$lines[] = 'Sitemap: '.url($sitemap);
$lines[] = 'Allow: /llms.txt';
$lines[] = 'Allow: /posts/*.md';
return response(implode("\n", $lines)."\n")->header('Content-Type', 'text/plain; charset=utf-8');
}
+3 -6
View File
@@ -2,7 +2,6 @@
declare(strict_types=1);
namespace App\Blog\Controllers;
use App\Models\Post;
@@ -11,11 +10,9 @@ class HomeController extends Controller
{
public function index()
{
$posts = Post::query()
->published()
->orderByDesc('is_sticky')
->latest('published_at')
->paginate((int) blog_setting('per_page', config('blog.per_page')));
$posts = blog_paginate(
Post::query()->published()->orderedForList()
);
return theme_view('index', compact('posts'));
}
+120
View File
@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace App\Blog\Controllers;
use App\Models\Post;
use Illuminate\Support\Str;
use League\HTMLToMarkdown\HtmlConverter;
class LlmsController extends Controller
{
/**
* /llms.txt — live index for agents. Not a static file.
*/
public function index()
{
$siteName = (string) blog_setting('site_name', config('blog.name'));
$siteDescription = (string) blog_setting('site_description', config('blog.description'));
$posts = Post::query()
->published()
->latest('published_at')
->limit(200)
->get();
$lines = [
'# '.$siteName,
'',
'> '.$siteDescription,
'',
'## Feeds',
'',
'- [RSS]('.route('feed.rss').')',
'- [Sitemap]('.route('feed.sitemap').')',
'- [llms.txt]('.route('feed.llms').')',
'',
'## Articles',
'',
];
foreach ($posts as $post) {
if ($this->isRestricted($post)) {
continue;
}
$summary = Str::limit(trim(preg_replace('/\s+/', ' ', strip_tags($post->excerpt_or_fallback)) ?? ''), 120);
$markdownUrl = $this->markdownUrl($post);
$htmlUrl = $post->url;
$title = $post->title;
$lines[] = $markdownUrl
? '- ['.$title.']('.$markdownUrl.') — '.$summary.' ([html]('.$htmlUrl.'))'
: '- ['.$title.']('.$htmlUrl.') — '.$summary;
}
return response(implode("\n", $lines)."\n", 200, [
'Content-Type' => 'text/plain; charset=utf-8',
'Cache-Control' => 'no-store',
]);
}
/**
* /posts/{slug}.md — full markdown for a published public post.
*/
public function postMarkdown(string $slug)
{
$post = Post::query()
->where('slug', $slug)
->orWhere('id', (int) $slug)
->first();
abort_unless($post && $post->status === 'published' && ! $this->isRestricted($post), 404);
$body = $post->content_format === 'markdown'
? (string) $post->content
: $this->htmlToMarkdown((string) $post->content);
$out = '# '.$post->title."\n\n";
if ($post->excerpt) {
$out .= '> '.$post->excerpt."\n\n";
}
$out .= $body."\n";
return response($out, 200, [
'Content-Type' => 'text/markdown; charset=utf-8',
'Cache-Control' => 'no-store',
]);
}
private function isRestricted(Post $post): bool
{
if (filled($post->read_password)) {
return true;
}
return ! empty(($post->meta ?? [])['members_only']);
}
private function markdownUrl(Post $post): ?string
{
$slug = trim((string) $post->slug);
if ($slug === '') {
return route('geo.post.markdown', ['slug' => (string) $post->id]);
}
return route('geo.post.markdown', ['slug' => $slug]);
}
private function htmlToMarkdown(string $html): string
{
$converter = new HtmlConverter([
'strip_tags' => false,
'hard_break' => true,
]);
return $converter->convert($html);
}
}
+15 -6
View File
@@ -2,17 +2,15 @@
declare(strict_types=1);
namespace App\Blog\Controllers;
use App\Blog\Services\PostContentRenderer;
use App\Models\Post;
use Illuminate\Pagination\Paginator;
class PostController extends Controller
{
public function __construct(private PostContentRenderer $renderer)
{
}
public function __construct(private PostContentRenderer $renderer) {}
public function show(string $id, ?string $page = null)
{
@@ -21,13 +19,16 @@ class PostController extends Controller
->orWhere('slug', $id)
->first();
abort_unless($post && $post->status === 'published', 404);
abort_unless($post, 404);
$canPreview = $this->canPreviewUnpublished();
abort_unless($post->status === 'published' || $canPreview, 404);
$this->ensureReadable($post);
// sablog 式 /show-{id}-{page}.shtml 的评论分页
if ($page !== null) {
\Illuminate\Pagination\Paginator::currentPageResolver(fn () => max(1, (int) $page));
Paginator::currentPageResolver(fn () => max(1, (int) $page));
}
// 浏览量由 /track-view 端点统计(页面整页缓存下控制器不执行,避免双计)
@@ -44,9 +45,17 @@ class PostController extends Controller
'comments' => $comments,
'contentHtml' => $rendered['html'],
'toc' => $rendered['toc'],
'isPreview' => $post->status !== 'published' && $canPreview,
]);
}
private function canPreviewUnpublished(): bool
{
$user = auth()->user();
return $user && ($user->isAdmin() || $user->hasRole('editor'));
}
private function ensureReadable(Post $post): void
{
if (! $post->read_password) {
+3 -6
View File
@@ -2,7 +2,6 @@
declare(strict_types=1);
namespace App\Blog\Controllers;
use App\Models\Post;
@@ -13,11 +12,9 @@ class SearchController extends Controller
{
$keyword = trim(request('q', request('keyword', '')));
$posts = Post::query()
->published()
->search($keyword)
->orderByDesc('published_at')
->paginate((int) blog_setting('per_page', config('blog.per_page')));
$posts = blog_paginate(
Post::query()->published()->search($keyword)->orderedForList()
);
return theme_view('search', compact('posts', 'keyword'));
}
+8 -7
View File
@@ -2,17 +2,17 @@
declare(strict_types=1);
namespace App\Blog\Controllers;
use App\Models\Post;
use Illuminate\Support\Facades\DB;
use Spatie\Tags\Tag;
class TagController extends Controller
{
public function index()
{
$counts = \Illuminate\Support\Facades\DB::table('taggables')
$counts = DB::table('taggables')
->selectRaw('tag_id, COUNT(*) as total')
->where('taggable_type', Post::class)
->groupBy('tag_id')
@@ -37,11 +37,12 @@ class TagController extends Controller
abort_unless($tag, 404);
$posts = Post::query()
->whereHas('tags', fn ($q) => $q->where('tags.id', $tag->id))
->published()
->latest('published_at')
->paginate((int) blog_setting('per_page', config('blog.per_page')));
$posts = blog_paginate(
Post::query()
->whereHas('tags', fn ($q) => $q->where('tags.id', $tag->id))
->published()
->orderedForList()
);
return theme_view('tag', compact('tag', 'posts'));
}
+2 -1
View File
@@ -6,6 +6,7 @@ declare(strict_types=1);
namespace App\Blog\Services;
use App\Blog\Support\MarkdownMediaEmbed;
use App\Blog\Support\SablogHtmlNormalizer;
use App\Models\Post;
use Illuminate\Support\Facades\Cache;
use League\CommonMark\Environment\Environment;
@@ -38,7 +39,7 @@ class PostContentRenderer
public function render(Post $post): string
{
$embed = app(MarkdownMediaEmbed::class);
$raw = (string) $post->content;
$raw = app(SablogHtmlNormalizer::class)->normalize((string) $post->content);
$content = $post->content_format === 'markdown'
? $this->toHtml($embed->expandMarkdown($raw))
+69
View File
@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace App\Blog\Support;
use Illuminate\Http\Request;
/**
* Homepage / list browse mode, matching old SaBlog 标准 | 列表.
*/
class HomeViewMode
{
public const LIST = 'list';
public const FULL = 'full';
public const COOKIE = 'll_home_view';
/**
* @return array<string, string>
*/
public static function options(): array
{
return [
self::LIST => '列表',
self::FULL => '标准',
];
}
public static function isValid(mixed $value): bool
{
return is_string($value) && in_array($value, [self::LIST, self::FULL], true);
}
public static function resolve(?Request $request = null): string
{
$request ??= request();
foreach ([
$request->query('view'),
$request->cookie(self::COOKIE),
blog_setting('home_view_mode', config('blog.home_view_mode', self::LIST)),
] as $value) {
if (self::isValid($value)) {
return $value;
}
}
return self::LIST;
}
public static function current(): string
{
$request = request();
$mode = self::resolve($request);
if (self::isValid($request->query('view'))) {
cookie()->queue(cookie(self::COOKIE, (string) $request->query('view'), 60 * 24 * 30));
}
return $mode;
}
public static function url(string $mode): string
{
return request()->fullUrlWithQuery(['view' => $mode]);
}
}
+49 -7
View File
@@ -45,7 +45,7 @@ class PageCacheMiddleware
'body' => $response->getContent(),
'type' => $response->headers->get('Content-Type') ?: 'text/html; charset=utf-8',
], $ttl);
self::rememberKey($key);
self::rememberKey($key, $request->fullUrl());
}
return $response;
@@ -53,8 +53,8 @@ class PageCacheMiddleware
public static function flush(): void
{
foreach ((array) Cache::get(self::TAG.'_keys', []) as $key) {
Cache::forget($key);
foreach (array_keys((array) Cache::get(self::TAG.'_keys', [])) as $key) {
Cache::forget((string) $key);
}
Cache::forget(self::TAG.'_keys');
@@ -88,16 +88,50 @@ class PageCacheMiddleware
Cache::forget(self::TAG.'_misses');
}
/**
* @return list<array{key: string, url: string, cached_at: string}>
*/
public static function entries(): array
{
$entries = [];
foreach ((array) Cache::get(self::TAG.'_keys', []) as $key => $meta) {
$entries[] = [
'key' => (string) $key,
'url' => is_array($meta) ? (string) ($meta['url'] ?? $key) : (string) $key,
'cached_at' => is_array($meta) ? (string) ($meta['cached_at'] ?? '') : '',
];
}
usort($entries, fn (array $left, array $right): int => strcmp($right['cached_at'], $left['cached_at']));
return $entries;
}
public static function forget(string $key): void
{
Cache::forget($key);
$keys = (array) Cache::get(self::TAG.'_keys', []);
unset($keys[$key]);
Cache::put(self::TAG.'_keys', $keys, now()->addDay());
}
private static function increment(string $metric): void
{
$key = self::TAG.'_'.$metric;
Cache::put($key, (int) Cache::get($key, 0) + 1, now()->addDay());
}
private static function rememberKey(string $key): void
private static function rememberKey(string $key, ?string $url = null): void
{
$keys = (array) Cache::get(self::TAG.'_keys', []);
$keys[$key] = true;
$previous = $keys[$key] ?? [];
$keys[$key] = [
'url' => $url ?: (is_array($previous) ? (string) ($previous['url'] ?? '') : ''),
'cached_at' => now()->toDateTimeString(),
];
Cache::put(self::TAG.'_keys', $keys, now()->addDay());
}
@@ -117,16 +151,24 @@ class PageCacheMiddleware
return false;
}
// 动态端点
// 动态端点、订阅和 agent 索引不进整页缓存,新文章要马上能看到
if (str_starts_with($path, 'track-view')) {
return false;
}
if (in_array($path, ['rss.xml', 'rss.php', 'sitemap.xml', 'robots.txt', 'llms.txt'], true)) {
return false;
}
if (str_ends_with($path, '.md')) {
return false;
}
return true;
}
private function key(Request $request): string
{
return self::TAG.':'.md5($request->fullUrl());
return self::TAG.':'.md5($request->fullUrl().'|view:'.HomeViewMode::resolve($request));
}
}
+376
View File
@@ -0,0 +1,376 @@
<?php
declare(strict_types=1);
namespace App\Blog\Support;
use App\Blog\Services\PostContentRenderer;
use DOMDocument;
use DOMElement;
use DOMNode;
use DOMXPath;
use League\HTMLToMarkdown\HtmlConverter;
use Throwable;
/**
* Cleans leftover SaBlog-X HTML so imported posts render without literal &nbsp;
* and without SyntaxHighlighter markup stretching the page.
*/
class SablogHtmlNormalizer
{
/**
* @return array<string, string>
*/
private function languageAliases(): array
{
return [
'js' => 'javascript',
'javascript' => 'javascript',
'xml' => 'xml',
'html' => 'xml',
'xhtml' => 'xml',
'css' => 'css',
'php' => 'php',
'sql' => 'sql',
'java' => 'java',
'py' => 'python',
'python' => 'python',
'c#' => 'csharp',
'cs' => 'csharp',
'bash' => 'bash',
'shell' => 'bash',
'sh' => 'bash',
'text' => 'text',
];
}
public function normalize(string $content): string
{
if ($content === '') {
return '';
}
$content = $this->decodeNbsp($content);
if (! preg_match('/<\s*(div|p|ol|ul|li|pre|code|span|br|table)\b/i', $content)) {
return $this->recoverFlattenedShell($content);
}
try {
$content = $this->transformHtml($content);
} catch (Throwable) {
// Keep the decoded text when HTML parsing fails.
}
return $this->recoverFlattenedShell($content);
}
public function toMarkdown(string $html, HtmlConverter $converter, PostContentRenderer $renderer): string
{
return $converter->convert($renderer->toMarkdownSafeTokens($this->normalize($html)));
}
public function plainText(string $content): string
{
$text = $this->decodeNbsp(strip_tags($content));
$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$text = str_replace("\xC2\xA0", ' ', $text);
$text = preg_replace('/\s+/u', ' ', $text) ?? $text;
return trim($text);
}
public function decodeNbsp(string $content): string
{
$content = str_replace(
['&amp;nbsp;', '&nbsp;', '&#160;', '&#xA0;', '&#xa0;', "\xC2\xA0"],
' ',
$content
);
return str_replace('&amp;nbsp;', ' ', $content);
}
private function transformHtml(string $html): string
{
$document = $this->loadDocument($html);
$xpath = new DOMXPath($document);
$this->convertHighlighterBlocks($document, $xpath);
$this->convertHighlighterLists($document, $xpath);
$this->convertStackedCodeDivs($document);
$this->removeEmptyBlocks($xpath);
return $this->innerHtml($document);
}
private function convertHighlighterBlocks(DOMDocument $document, DOMXPath $xpath): void
{
$nodes = [];
foreach ($xpath->query('//*[contains(concat(" ", normalize-space(@class), " "), " codeText ")]') ?: [] as $node) {
if ($node instanceof DOMElement) {
$nodes[] = $node;
}
}
foreach ($nodes as $node) {
if ($node->parentNode === null) {
continue;
}
$this->replaceWithPre($document, $xpath, $node);
}
}
private function convertHighlighterLists(DOMDocument $document, DOMXPath $xpath): void
{
$lists = [];
foreach ($xpath->query('//ol[contains(@class, "dp-")]') ?: [] as $node) {
if (! $node instanceof DOMElement || $node->parentNode === null) {
continue;
}
if ($this->closestTag($node, 'pre') !== null) {
continue;
}
$lists[] = $node;
}
foreach ($lists as $list) {
$this->replaceWithPre($document, $xpath, $list);
}
}
private function convertStackedCodeDivs(DOMDocument $document): void
{
$xpath = new DOMXPath($document);
$parents = [];
foreach ($xpath->query('//*') ?: [] as $node) {
if ($node instanceof DOMElement) {
$parents[] = $node;
}
}
foreach ($parents as $parent) {
if ($parent->parentNode === null) {
continue;
}
if (in_array(strtolower($parent->nodeName), ['pre', 'code'], true)) {
continue;
}
$run = [];
$flush = function () use (&$run, $document): void {
if (count($run) < 2) {
$run = [];
return;
}
$lines = array_map(fn (DOMElement $div): string => $this->plainText($this->innerHtmlOf($div)), $run);
if (! $this->looksLikeCodeLines($lines)) {
$run = [];
return;
}
$first = $run[0];
$pre = $this->makePre($document, implode("\n", $lines), $this->detectLanguage('', implode("\n", $lines)));
$first->parentNode?->insertBefore($pre, $first);
foreach ($run as $div) {
$div->parentNode?->removeChild($div);
}
$run = [];
};
foreach (iterator_to_array($parent->childNodes) as $child) {
if ($child instanceof DOMNode && $child->nodeType === XML_TEXT_NODE && trim((string) $child->textContent) === '') {
continue;
}
if ($child instanceof DOMElement && strtolower($child->nodeName) === 'div' && $this->isSimpleLineDiv($child)) {
$run[] = $child;
continue;
}
$flush();
}
$flush();
}
}
private function removeEmptyBlocks(DOMXPath $xpath): void
{
foreach ($xpath->query('//p|//div') ?: [] as $node) {
if (! $node instanceof DOMElement || $node->parentNode === null) {
continue;
}
if ($node->getElementsByTagName('*')->length > 0) {
continue;
}
$text = trim(str_replace("\xC2\xA0", ' ', $node->textContent));
if ($text === '') {
$node->parentNode->removeChild($node);
}
}
}
private function replaceWithPre(DOMDocument $document, DOMXPath $xpath, DOMElement $node): void
{
$hint = '';
if (preg_match('/dp-([a-z0-9#+]+)/i', $node->getAttribute('class'), $match)) {
$hint = $match[1];
}
$lines = [];
foreach ($xpath->query('.//li', $node) ?: [] as $item) {
$lines[] = $this->plainText($document->saveHTML($item) ?: '');
}
if ($lines === []) {
foreach ($xpath->query('.//*[contains(@class, "codeHead")]', $node) ?: [] as $head) {
$head->parentNode?->removeChild($head);
}
$lines = preg_split('/\R/u', $this->plainText($node->textContent)) ?: [];
}
$code = implode("\n", $lines);
$pre = $this->makePre($document, $code, $this->detectLanguage($hint, $code));
$node->parentNode?->replaceChild($pre, $node);
}
private function makePre(DOMDocument $document, string $code, string $language): DOMElement
{
$pre = $document->createElement('pre');
$codeNode = $document->createElement('code');
$codeNode->setAttribute('class', 'language-'.$language);
$codeNode->appendChild($document->createTextNode($code));
$pre->appendChild($codeNode);
return $pre;
}
/**
* @param list<string> $lines
*/
private function looksLikeCodeLines(array $lines): bool
{
$codeish = 0;
foreach ($lines as $line) {
if ($line === '') {
continue;
}
if (preg_match('/\\\\$/', $line)
|| preg_match('/^\s*(docker|npm|npx|pnpm|yarn|git|curl|wget|sudo|ssh|kubectl|composer|php|python|pip|node)\b/i', $line)
|| preg_match('/^\s+--[\w-]+/', $line)
|| preg_match('/^\s*\|/', $line)
|| preg_match('/[{};]$/', $line)
) {
$codeish++;
}
}
return $codeish >= 2;
}
private function isSimpleLineDiv(DOMElement $div): bool
{
foreach ($div->childNodes as $child) {
if ($child instanceof DOMElement && ! in_array(strtolower($child->nodeName), ['br', 'span', 'code', 'b', 'i', 'em', 'strong'], true)) {
return false;
}
}
$text = $this->plainText($div->textContent);
return $text !== '';
}
private function detectLanguage(string $hint, string $code): string
{
if (preg_match('/^\s*(docker|kubectl|npm|npx|pnpm|yarn|composer|curl|wget|sudo|ssh)\b/m', $code)) {
return 'bash';
}
$aliases = $this->languageAliases();
$key = strtolower(trim($hint));
return $aliases[$key] ?? ($key !== '' ? $key : 'text');
}
/**
* Recover a docker/shell command that HtmlConverter flattened into one line.
*/
private function recoverFlattenedShell(string $content): string
{
if (str_contains($content, '<pre')) {
return $content;
}
return preg_replace_callback(
'/(?:docker|sudo docker)\s+run\b(?:[^<\n]*\\\\)+\s*[^<\n]+/i',
function (array $match): string {
$code = trim($this->plainText($match[0]));
$code = preg_replace('/\\\\\s+/', "\\\n", $code) ?? $code;
return '<pre><code class="language-bash">'.e($code).'</code></pre>';
},
$content
) ?? $content;
}
private function closestTag(DOMNode $node, string $tag): ?DOMElement
{
$current = $node->parentNode;
while ($current instanceof DOMElement) {
if (strtolower($current->nodeName) === $tag) {
return $current;
}
$current = $current->parentNode;
}
return null;
}
private function loadDocument(string $html): DOMDocument
{
$document = new DOMDocument('1.0', 'UTF-8');
$previous = libxml_use_internal_errors(true);
$wrapped = '<div id="ll-root">'.$html.'</div>';
$document->loadHTML('<?xml encoding="UTF-8">'.$wrapped, LIBXML_NOERROR | LIBXML_HTML_NODEFDTD);
libxml_clear_errors();
libxml_use_internal_errors($previous);
return $document;
}
private function innerHtml(DOMDocument $document): string
{
$root = $document->getElementById('ll-root');
if (! $root instanceof DOMElement) {
$root = (new DOMXPath($document))->query('//*[@id="ll-root"]')->item(0);
}
if (! $root instanceof DOMElement) {
return '';
}
return $this->innerHtmlOf($root);
}
private function innerHtmlOf(DOMElement $element): string
{
$html = '';
foreach ($element->childNodes as $child) {
$html .= $element->ownerDocument?->saveHTML($child) ?: '';
}
return $html;
}
}
+3 -1
View File
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Console\Commands;
use App\Blog\Support\SablogHtmlNormalizer;
use App\Models\Post;
use Illuminate\Console\Command;
use League\HTMLToMarkdown\HtmlConverter;
@@ -36,6 +37,7 @@ class ConvertContent extends Command
]);
$renderer = app(\App\Blog\Services\PostContentRenderer::class);
$normalizer = app(SablogHtmlNormalizer::class);
$posts = $query->get();
if ($posts->isEmpty()) {
@@ -49,7 +51,7 @@ class ConvertContent extends Command
foreach ($posts as $post) {
// [attach=xx] 是 markdown 保留字符,先换成令牌,渲染时再解析
$markdown = $converter->convert($renderer->toMarkdownSafeTokens($post->content));
$markdown = $normalizer->toMarkdown((string) $post->content, $converter, $renderer);
if ($this->option('dry-run')) {
$this->newLine();
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Blog\Support\SablogHtmlNormalizer;
use App\Models\Post;
use Illuminate\Console\Command;
class FixImportedContent extends Command
{
protected $signature = 'sablog:fix-content
{--all : Repair every post}
{id? : Single post ID}
{--dry-run : Preview only}';
protected $description = 'Clean leftover SaBlog HTML: literal &nbsp;, highlighter blocks, stacked docker divs';
public function handle(SablogHtmlNormalizer $normalizer): int
{
$query = Post::query();
if ($id = $this->argument('id')) {
$query->where('id', $id);
} elseif (! $this->option('all')) {
$this->error('Pass a post ID or use --all');
return self::FAILURE;
}
$posts = $query->get();
if ($posts->isEmpty()) {
$this->info('No posts to repair');
return self::SUCCESS;
}
$changed = 0;
foreach ($posts as $post) {
$cleaned = $normalizer->normalize((string) $post->content);
if ($cleaned === $post->content) {
continue;
}
if ($this->option('dry-run')) {
$this->line("[{$post->id}] {$post->title}");
continue;
}
$post->content = $cleaned;
$post->save();
$changed++;
}
$this->info($this->option('dry-run')
? 'Preview only, nothing written'
: "Repaired {$changed} posts");
return self::SUCCESS;
}
}
+8 -7
View File
@@ -2,19 +2,18 @@
declare(strict_types=1);
namespace App\Console\Commands;
use App\Blog\Services\AttachmentImporter;
use App\Models\Category;
use App\Models\Comment;
use App\Models\Link;
use App\Blog\Services\PostContentRenderer;
use App\Blog\Support\SablogHtmlNormalizer;
use App\Models\Post;
use App\Models\Setting;
use App\Models\User;
use App\Support\SlugGenerator;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use League\HTMLToMarkdown\HtmlConverter;
use PDO;
use Spatie\Tags\Tag;
@@ -271,6 +270,7 @@ class SablogImport extends Command
'keywords' => $row['keywords'] ?: null,
'status' => $status,
'is_sticky' => (bool) $row['stick'],
'sticky_level' => ((bool) $row['stick']) ? 1 : 0,
'close_comment' => (bool) $row['closecomment'],
'read_password' => $row['readpassword'] ?: null,
'views' => (int) $row['views'],
@@ -362,15 +362,16 @@ class SablogImport extends Command
{
$this->info('转换文章内容为 Markdown ...');
$converter = new \League\HTMLToMarkdown\HtmlConverter([
$converter = new HtmlConverter([
'strip_tags' => false,
'hard_break' => true,
]);
$renderer = app(\App\Blog\Services\PostContentRenderer::class);
$renderer = app(PostContentRenderer::class);
$normalizer = app(SablogHtmlNormalizer::class);
$count = 0;
foreach (Post::query()->where('content_format', 'html')->cursor() as $post) {
$post->content = $converter->convert($renderer->toMarkdownSafeTokens($post->content));
$post->content = $normalizer->toMarkdown((string) $post->content, $converter, $renderer);
$post->content_format = 'markdown';
$post->save();
$count++;
+8
View File
@@ -42,6 +42,7 @@ class BlogSettings extends Page
// 外观
'active_theme' => Setting::get('active_theme', config('themes.default')),
'per_page' => Setting::get('per_page', config('blog.per_page')),
'home_view_mode' => Setting::get('home_view_mode', config('blog.home_view_mode', 'list')),
'list_shownum' => Setting::get('list_shownum', '50'),
'listtime' => Setting::get('listtime', 'Y-m-d'),
'normaltime' => Setting::get('normaltime', 'Y-m-d H:i'),
@@ -102,6 +103,13 @@ class BlogSettings extends Page
->columnSpanFull()
->options(collect(app(\App\Blog\Support\ThemeManager::class)->all())->pluck('title', 'name')),
TextInput::make('per_page')->label('首页每页文章数')->numeric(),
Select::make('home_view_mode')
->label('首页浏览模式')
->options([
'list' => '列表(标题 + 摘要)',
'full' => '标准(全文)',
])
->helperText('前台右上角仍可临时切换,和老 sablog 的「标准 | 列表」一样'),
TextInput::make('list_shownum')->label('列表每页数量')->numeric()->helperText('分类/标签/搜索等列表页'),
TextInput::make('listtime')->label('列表时间格式')->placeholder('Y-m-d'),
TextInput::make('normaltime')->label('详情时间格式')->placeholder('Y-m-d H:i'),
+91
View File
@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace App\Filament\Pages;
use Filament\Pages\Page;
use Illuminate\Support\Facades\File;
class Languages extends Page
{
protected static \UnitEnum|string|null $navigationGroup = '管理';
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-language';
protected static ?string $navigationLabel = '语言';
protected static ?string $title = '语言';
protected static ?string $slug = 'languages';
protected static ?int $navigationSort = 21;
protected string $view = 'filament.pages.languages';
/**
* @return array{
* locale: string,
* fallback: string,
* faker: string,
* app_files: list<string>,
* filament_locales: list<string>,
* content_i18n: bool
* }
*/
public function getInfoProperty(): array
{
return [
'locale' => app()->getLocale(),
'fallback' => (string) config('app.fallback_locale'),
'faker' => (string) config('app.faker_locale'),
'app_files' => $this->appLanguageFiles(),
'filament_locales' => $this->filamentLocales(),
'content_i18n' => false,
];
}
/**
* @return list<string>
*/
private function appLanguageFiles(): array
{
$found = [];
foreach (File::glob(lang_path('*.json')) as $file) {
$found[] = basename((string) $file, '.json');
}
foreach (File::directories(lang_path()) as $directory) {
$name = basename($directory);
if ($name === 'vendor') {
continue;
}
$found[] = $name;
}
$found = array_values(array_unique($found));
sort($found);
return $found;
}
/**
* @return list<string>
*/
private function filamentLocales(): array
{
$path = lang_path('vendor/filament');
if (! File::isDirectory($path)) {
return [];
}
$locales = array_map(
static fn (string $directory): string => basename($directory),
File::directories($path)
);
sort($locales);
return $locales;
}
}
+93
View File
@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace App\Filament\Pages;
use App\Blog\Support\PageCacheMiddleware;
use App\Models\Setting;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Illuminate\Support\Facades\Cache;
class PageCache extends Page
{
protected static \UnitEnum|string|null $navigationGroup = '管理';
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-bolt';
protected static ?string $navigationLabel = '页面缓存';
protected static ?string $title = '页面缓存';
protected static ?string $slug = 'page-cache';
protected static ?int $navigationSort = 20;
protected string $view = 'filament.pages.page-cache';
/**
* @return array{hits: int, misses: int, count: int, rate: float, ttl: int}
*/
public function getStatsProperty(): array
{
return [
...PageCacheMiddleware::stats(),
'ttl' => (int) config('blog.page_cache_ttl', 300),
];
}
/**
* @return list<array{key: string, url: string, cached_at: string}>
*/
public function getEntriesProperty(): array
{
return PageCacheMiddleware::entries();
}
public function flushPageCache(): void
{
PageCacheMiddleware::flush();
Notification::make()->title('前台页面缓存已清空')->success()->send();
}
public function flushAllCache(): void
{
PageCacheMiddleware::flush();
Setting::flushCache();
Cache::flush();
Notification::make()->title('全部缓存已清空')->success()->send();
}
public function resetStats(): void
{
PageCacheMiddleware::resetStats();
Notification::make()->title('命中统计已重置')->success()->send();
}
public function forgetEntry(string $key): void
{
PageCacheMiddleware::forget($key);
Notification::make()->title('已删除这条缓存')->success()->send();
}
protected function getHeaderActions(): array
{
return [
Action::make('resetStats')
->label('重置统计')
->color('gray')
->action(fn () => $this->resetStats()),
Action::make('flushPageCache')
->label('清空前台缓存')
->color('warning')
->action(fn () => $this->flushPageCache()),
Action::make('flushAllCache')
->label('清空全部缓存')
->color('danger')
->requiresConfirmation()
->action(fn () => $this->flushAllCache()),
];
}
}
@@ -46,7 +46,8 @@ class CategoryForm
->label('封面图')
->module('articles')
->mediaType('image')
->relationship('coverMedia'),
->relationship('coverMedia')
->extraFieldWrapperAttributes(['class' => 'll-media-picker-preview']),
]),
]);
}
@@ -2,10 +2,10 @@
declare(strict_types=1);
namespace App\Filament\Resources\Posts\Pages;
use App\Filament\Resources\Posts\PostResource;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
@@ -16,6 +16,12 @@ class EditPost extends EditRecord
protected function getHeaderActions(): array
{
return [
Action::make('preview')
->label('预览')
->icon('heroicon-o-eye')
->color('gray')
->url(fn (): string => $this->record->url)
->openUrlInNewTab(),
DeleteAction::make(),
];
}
@@ -5,10 +5,11 @@ declare(strict_types=1);
namespace App\Filament\Resources\Posts\RelationManagers;
use App\Models\Comment;
use Filament\Forms\Components\TextInput;
use Filament\Actions\Action;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables\Actions\Action;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
@@ -22,7 +23,7 @@ class CommentsRelationManager extends RelationManager
protected static ?string $pluralModelLabel = '评论';
public function form(\Filament\Schemas\Schema $schema): \Filament\Schemas\Schema
public function form(Schema $schema): Schema
{
return $schema
->components([
+270 -104
View File
@@ -5,129 +5,295 @@ declare(strict_types=1);
namespace App\Filament\Resources\Posts\Schemas;
use App\Blog\Support\MarkdownMediaEmbed;
use App\Models\Post;
use Filament\Actions\Action;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\MarkdownEditor;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Notifications\Notification;
use Filament\Schemas\Components\Actions;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema;
use ReflectionClass;
use Xpier\FilamentMediaLibrary\Components\MediaPicker;
use Xpier\FilamentMediaLibrary\Models\MediaLibrary;
class PostForm
{
public const SIDEBAR_KEY = 'post_form_sidebar';
public const ACCESS_KEY = 'post_form_access';
public static function configure(Schema $schema): Schema
{
return $schema
->columns(self::pcColumns(3))
->components([
\Filament\Schemas\Components\Section::make('内容')
->columns(2)
->schema([
TextInput::make('title')
->label('标题')
->required()
->columnSpanFull(),
TextInput::make('slug')
->label('Slug(留空自动生成)'),
Select::make('category_id')
->label('分类')
->relationship('category', 'name'),
Select::make('content_format')
->label('内容格式')
->options([
'markdown' => 'Markdown',
'html' => 'HTML',
])
->default('markdown')
->live(),
Select::make('status')
->label('状态')
->options([
'published' => '已发布',
'draft' => '草稿',
'private' => '私密',
])
->default('published'),
MarkdownEditor::make('content')
->label('正文')
->required()
->helperText('Markdown 插入图片用 ![说明](media:ID),文件用 [文件名](media:ID)。不要把 CDN / 对象存储地址写进正文。')
->fileAttachments(false)
->columnSpanFull(),
MediaPicker::make('insert_media_id')
->label('从媒体库插入正文')
->module('articles')
->mediaType(null)
->dehydrated(false)
->helperText('选好媒体后点「插入到正文」。Markdown 写入 media:ID 引用,渲染时再换成真实地址。')
->columnSpanFull(),
Actions::make([
Action::make('insertSelectedMedia')
->label('插入到正文')
->action(function (Get $get, Set $set): void {
$mediaId = $get('insert_media_id');
$media = is_numeric($mediaId)
? MediaLibrary::query()->find((int) $mediaId)
: null;
if (! $media instanceof MediaLibrary) {
Notification::make()->title('请先从媒体库选择文件')->warning()->send();
return;
}
$snippet = app(MarkdownMediaEmbed::class)->snippet(
$media,
(string) ($get('content_format') ?: 'markdown')
);
$set('content', rtrim((string) $get('content'))."\n\n".$snippet."\n");
$set('insert_media_id', null);
Notification::make()->title('已写入正文引用')->success()->send();
}),
])->columnSpanFull(),
]),
\Filament\Schemas\Components\Section::make('封面与摘要')
->columns(2)
->schema([
MediaPicker::make('cover_media_id')
->label('封面图')
->module('articles')
->mediaType('image')
->relationship('coverMedia')
->columnSpanFull(),
Textarea::make('excerpt')
->label('摘要(留空自动截取)')
->rows(3),
TextInput::make('keywords')
->label('关键词(逗号分隔)'),
]),
\Filament\Schemas\Components\Section::make('发布设置')
->columns(3)
->schema([
DateTimePicker::make('published_at')
->label('发布时间')
->default(now()),
Toggle::make('is_sticky')
->label('置顶'),
Toggle::make('close_comment')
->label('关闭评论'),
TextInput::make('read_password')
->label('阅读密码(留空公开)'),
TextInput::make('views')
->label('浏览量')
->numeric()
->disabled(),
TextInput::make('comment_count')
->label('评论数')
->numeric()
->disabled(),
]),
self::contentSection(),
self::sidebarGroup(),
]);
}
/**
* Append sections or fields into the right-hand sidebar (2:1 layout).
*
* @param array<Component> $components
*/
public static function appendToSidebar(Schema $schema, array $components): void
{
$sidebar = self::findKeyedComponent($schema->getComponents(), self::SIDEBAR_KEY);
if (! $sidebar instanceof Component) {
$schema->components([
...$schema->getComponents(),
...$components,
]);
return;
}
$existing = $sidebar->getDefaultChildComponents();
if ($existing instanceof Schema) {
$existing->components([
...$existing->getComponents(),
...$components,
]);
return;
}
$sidebar->schema([
...(is_array($existing) ? $existing : []),
...$components,
]);
}
/**
* Append fields into the access group (password / members / price).
*
* @param array<Component> $fields
*/
public static function appendToAccessSection(Schema $schema, array $fields): void
{
$sidebar = self::findKeyedComponent($schema->getComponents(), self::SIDEBAR_KEY);
$access = $sidebar instanceof Component
? self::findKeyedComponent(self::childList($sidebar), self::ACCESS_KEY)
: null;
if (! $access instanceof Component) {
self::appendToSidebar($schema, [
Section::make('访问设置')
->key(self::ACCESS_KEY)
->schema($fields),
]);
return;
}
$existing = $access->getDefaultChildComponents();
if ($existing instanceof Schema) {
$existing->components([
...$existing->getComponents(),
...$fields,
]);
return;
}
$access->schema([
...(is_array($existing) ? $existing : []),
...$fields,
]);
}
private static function contentSection(): Section
{
return Section::make('正文')
->columnSpan(self::pcColumns(2))
->columns(self::pcColumns(3))
->schema([
TextInput::make('title')
->label('标题')
->required()
->columnSpanFull(),
TextInput::make('slug')
->label('Slug(留空自动生成)')
->columnSpanFull(),
Select::make('category_id')
->label('分类')
->relationship('category', 'name'),
ToggleButtons::make('content_format')
->label('内容格式')
->options([
'markdown' => 'Markdown',
'html' => 'HTML',
])
->default('markdown')
->inline()
->grouped()
->live(),
ToggleButtons::make('status')
->label('状态')
->options([
'published' => '已发布',
'draft' => '草稿',
'private' => '私密',
])
->default('published')
->inline()
->grouped(),
MediaPicker::make('cover_media_id')
->label('封面图')
->module('articles')
->mediaType('image')
->relationship('coverMedia')
->extraFieldWrapperAttributes(['class' => 'll-media-picker-preview'])
->columnSpanFull(),
MarkdownEditor::make('content')
->label('正文')
->required()
->minHeight('32rem')
->helperText('Markdown 插入图片用 ![说明](media:ID),文件用 [文件名](media:ID)。不要把 CDN / 对象存储地址写进正文。')
->fileAttachments(false)
->columnSpanFull(),
MediaPicker::make('insert_media_id')
->label('从媒体库插入正文')
->module('articles')
->mediaType(null)
->dehydrated(false)
->extraFieldWrapperAttributes(['class' => 'll-media-picker-preview'])
->helperText('选好媒体后点「插入到正文」。Markdown 写入 media:ID 引用,渲染时再换成真实地址。')
->columnSpanFull(),
Actions::make([
Action::make('insertSelectedMedia')
->label('插入到正文')
->action(function (Get $get, Set $set): void {
$mediaId = $get('insert_media_id');
$media = is_numeric($mediaId)
? MediaLibrary::query()->find((int) $mediaId)
: null;
if (! $media instanceof MediaLibrary) {
Notification::make()->title('请先从媒体库选择文件')->warning()->send();
return;
}
$snippet = app(MarkdownMediaEmbed::class)->snippet(
$media,
(string) ($get('content_format') ?: 'markdown')
);
$set('content', rtrim((string) $get('content'))."\n\n".$snippet."\n");
$set('insert_media_id', null);
Notification::make()->title('已写入正文引用')->success()->send();
}),
])->columnSpanFull(),
]);
}
private static function sidebarGroup(): Group
{
return Group::make([
Section::make('发布设置')
->schema([
DateTimePicker::make('published_at')
->label('发布时间')
->default(now()),
ToggleButtons::make('sticky_level')
->label('置顶等级')
->options(Post::stickyLevelOptions())
->default(Post::STICKY_NONE)
->inline()
->grouped()
->helperText('轻置顶压过普通文章,强置顶压过轻置顶,置顶头条永远排在最上面。'),
ToggleButtons::make('close_comment')
->label('评论状态')
->boolean('关闭评论', '开放评论')
->default(false)
->inline()
->grouped()
->dehydrateStateUsing(fn (mixed $state): bool => (bool) $state),
Textarea::make('excerpt')
->label('摘要(留空自动截取)')
->rows(4),
TextInput::make('keywords')
->label('关键词(逗号分隔)'),
]),
Section::make('访问设置')
->key(self::ACCESS_KEY)
->schema([
TextInput::make('read_password')
->label('阅读密码(留空公开)'),
]),
])
->key(self::SIDEBAR_KEY)
->columnSpan(self::pcColumns(1));
}
/**
* Admin writing is almost always on a PC. Filament's columns(3) only
* applies from the lg breakpoint, so we pin every breakpoint.
*
* @return array<string, int>
*/
private static function pcColumns(int $count): array
{
return [
'default' => $count,
'sm' => $count,
'md' => $count,
'lg' => $count,
'xl' => $count,
'2xl' => $count,
];
}
/**
* @param array<int, mixed> $components
*/
private static function findKeyedComponent(array $components, string $key): ?Component
{
foreach ($components as $component) {
if (! $component instanceof Component) {
continue;
}
if (self::rawKey($component) === $key) {
return $component;
}
}
return null;
}
private static function rawKey(Component $component): ?string
{
$property = (new ReflectionClass($component))->getProperty('key');
$key = $property->getValue($component);
return is_string($key) ? $key : null;
}
/**
* @return array<int, mixed>
*/
private static function childList(Component $component): array
{
$children = $component->getDefaultChildComponents();
if ($children instanceof Schema) {
return $children->getComponents();
}
return is_array($children) ? $children : [];
}
}
@@ -2,18 +2,18 @@
declare(strict_types=1);
namespace App\Filament\Resources\Posts\Tables;
use App\Models\Post;
use Filament\Actions\Action;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
use Filament\Forms\Components\Select;
use Filament\Tables\Columns\IconColumn;
use Filament\Support\Enums\Width;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Enums\FiltersLayout;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Filters\TernaryFilter;
use Filament\Tables\Table;
class PostsTable
@@ -46,9 +46,17 @@ class PostsTable
'draft' => 'warning',
default => 'gray',
}),
IconColumn::make('is_sticky')
TextColumn::make('sticky_level')
->label('置顶')
->boolean(),
->badge()
->sortable()
->formatStateUsing(fn ($state) => Post::stickyLevelOptions()[(int) $state] ?? '不置顶')
->color(fn ($state) => match ((int) $state) {
Post::STICKY_HEADLINE => 'danger',
Post::STICKY_STRONG => 'warning',
Post::STICKY_LIGHT => 'success',
default => 'gray',
}),
TextColumn::make('views')
->label('阅读')
->numeric()
@@ -79,12 +87,17 @@ class PostsTable
'markdown' => 'Markdown',
'html' => 'HTML',
]),
TernaryFilter::make('is_sticky')
->label('置顶'),
], \Filament\Tables\Enums\FiltersLayout::AboveContent)
SelectFilter::make('sticky_level')
->label('置顶')
->options(Post::stickyLevelOptions()),
], FiltersLayout::AboveContent)
->filtersFormColumns(3)
->filtersFormWidth(\Filament\Support\Enums\Width::ThreeExtraLarge)
->filtersFormWidth(Width::ThreeExtraLarge)
->recordActions([
Action::make('preview')
->label('预览')
->url(fn (Post $record): string => $record->url)
->openUrlInNewTab(),
ViewAction::make(),
EditAction::make(),
])
+4 -2
View File
@@ -2,9 +2,10 @@
declare(strict_types=1);
namespace App\Filament\Widgets;
use App\Blog\Support\PageCacheMiddleware;
use App\Filament\Pages\PageCache;
use App\Models\Comment;
use App\Models\Post;
use App\Models\User;
@@ -17,7 +18,7 @@ class BlogStats extends BaseWidget
protected function getStats(): array
{
$cache = \App\Blog\Support\PageCacheMiddleware::stats();
$cache = PageCacheMiddleware::stats();
return [
Stat::make('文章', Post::published()->count())
@@ -30,6 +31,7 @@ class BlogStats extends BaseWidget
->icon('heroicon-o-users'),
Stat::make('页面缓存', $cache['count'].' 条')
->description('命中率 '.$cache['rate'].'%(命中 '.$cache['hits'].' / 未命中 '.$cache['misses'].',按日滚动)')
->url(PageCache::getUrl())
->icon('heroicon-o-bolt'),
];
}
+61 -7
View File
@@ -2,25 +2,25 @@
declare(strict_types=1);
namespace App\Models;
use App\Support\MediaDisk;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Spatie\MediaLibrary\HasMedia;
use Xpier\FilamentMediaLibrary\Models\MediaLibrary;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Spatie\Tags\HasTags;
use Xpier\FilamentMediaLibrary\Models\MediaLibrary;
class Post extends Model implements HasMedia
{
use HasFactory;
use InteractsWithMedia;
use HasTags;
use InteractsWithMedia;
protected $fillable = [
'category_id',
@@ -34,6 +34,7 @@ class Post extends Model implements HasMedia
'cover_media_id',
'status',
'is_sticky',
'sticky_level',
'close_comment',
'read_password',
'views',
@@ -45,10 +46,63 @@ class Post extends Model implements HasMedia
protected $casts = [
'meta' => 'array',
'is_sticky' => 'boolean',
'sticky_level' => 'integer',
'close_comment' => 'boolean',
'published_at' => 'datetime',
];
public const STICKY_NONE = 0;
public const STICKY_LIGHT = 1;
public const STICKY_STRONG = 2;
public const STICKY_HEADLINE = 3;
/**
* @return array<int, string>
*/
public static function stickyLevelOptions(): array
{
return [
self::STICKY_NONE => '不置顶',
self::STICKY_LIGHT => '轻置顶',
self::STICKY_STRONG => '强置顶',
self::STICKY_HEADLINE => '置顶头条',
];
}
public function stickyLabel(): ?string
{
$level = (int) $this->sticky_level;
return $level > 0 ? (self::stickyLevelOptions()[$level] ?? '置顶') : null;
}
public function isPinned(): bool
{
return (int) $this->sticky_level > 0 || $this->is_sticky;
}
protected static function booted(): void
{
static::saving(function (Post $post): void {
$level = (int) ($post->sticky_level ?? 0);
$post->sticky_level = max(self::STICKY_NONE, min(self::STICKY_HEADLINE, $level));
if ($post->is_sticky && $post->sticky_level < self::STICKY_LIGHT) {
$post->sticky_level = self::STICKY_LIGHT;
}
$post->is_sticky = $post->sticky_level > self::STICKY_NONE;
});
}
public function scopeOrderedForList(Builder $query): Builder
{
return $query->orderByDesc('sticky_level')->latest('published_at');
}
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
@@ -107,8 +161,8 @@ class Post extends Model implements HasMedia
public function registerMediaCollections(): void
{
$this->addMediaCollection('attachments')->useDisk(\App\Support\MediaDisk::name());
$this->addMediaCollection('cover')->useDisk(\App\Support\MediaDisk::name())
$this->addMediaCollection('attachments')->useDisk(MediaDisk::name());
$this->addMediaCollection('cover')->useDisk(MediaDisk::name())
->singleFile()
->registerMediaConversions(function () {
$this->addMediaConversion('card')->width(1200)->height(630)->sharpen(1);
@@ -153,13 +207,13 @@ class Post extends Model implements HasMedia
return '';
}
$text = strip_tags($this->content);
$text = (new \App\Blog\Support\SablogHtmlNormalizer)->plainText((string) $this->content);
// 付费块不进入摘要,避免付费内容泄漏到列表页/SEO meta
$text = preg_replace('/\[paid\].*?\[\/paid\]/s', '', $text) ?? $text;
$text = preg_replace('/!\[([^\]]*)\]\((?:media|attach):\d+\)/i', '$1', $text) ?? $text;
$text = preg_replace('/\[([^\]]*)\]\((?:media|attach):\d+\)/i', '$1', $text) ?? $text;
$text = preg_replace('/\[[^\]]*\]/', '', $text) ?? $text;
return mb_substr($text, 0, 200);
return mb_substr(trim($text), 0, 200);
}
}
+39 -4
View File
@@ -4,10 +4,16 @@ declare(strict_types=1);
namespace App\Providers;
use App\Blog\Support\HomeViewMode;
use App\Blog\Support\PageCacheMiddleware;
use App\Blog\Support\PluginManager;
use App\Models\MarketPackage;
use Filament\Facades\Filament;
use Filament\View\PanelsRenderHook;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
use Plugins\Neatstudio\Payment\Models\Payment;
class AppServiceProvider extends ServiceProvider
{
@@ -21,16 +27,16 @@ class AppServiceProvider extends ServiceProvider
private function registerMarketPurchaseListener(): void
{
$manager = app(\App\Blog\Support\PluginManager::class);
$manager = app(PluginManager::class);
if (! $manager->isEnabled('neatstudio.payment')) {
return;
}
$manager->addAction('payment.paid', function (\Plugins\Neatstudio\Payment\Models\Payment $payment) {
$manager->addAction('payment.paid', function (Payment $payment) {
$payable = $payment->payable;
if ($payable instanceof \App\Models\MarketPackage) {
if ($payable instanceof MarketPackage) {
$payable->markPaid($payment);
}
}, 10);
@@ -54,7 +60,20 @@ class AppServiceProvider extends ServiceProvider
$this->registerMarketPurchaseListener();
// 前台页面缓存(匿名 GET,缓存含 SEO 注入后的最终 HTML)
app('router')->pushMiddlewareToGroup('web', \App\Blog\Support\PageCacheMiddleware::class);
app('router')->pushMiddlewareToGroup('web', PageCacheMiddleware::class);
View::composer([
'index',
'list',
'tag',
'search',
'partials.post-card',
'partials.view-mode-switcher',
], function ($view): void {
if (! $view->offsetExists('homeViewMode')) {
$view->with('homeViewMode', HomeViewMode::current());
}
});
// 后台紧凑化:侧边栏 + 表格行 + 筛选区(Filament 无官方全局紧凑开关,用 CSS 收紧)
Filament::serving(function () {
@@ -96,6 +115,22 @@ class AppServiceProvider extends ServiceProvider
/* 文章编辑框加大(MarkdownEditor 基于 tiptap) */
.fi-fo-markdown-editor .tiptap, .fi-fo-markdown-editor .ProseMirror { min-height: 26rem !important; }
/* 封面/媒体预览:禁止按栏宽 100% 拉伸,否则大图会把编辑页撑烂 */
.ll-media-picker-preview img,
img.h-32.max-w-full.rounded-md.object-contain {
max-width: 220px !important;
max-height: 124px !important;
width: auto !important;
height: auto !important;
object-fit: contain !important;
display: block !important;
}
.ll-media-picker-preview .relative,
.relative.rounded-lg:has(> img.h-32.max-w-full) {
width: fit-content;
max-width: 15rem;
}
/* 筛选条件区:整体一行横排,重置紧跟应用按钮,控件紧凑 */
.fi-ta-filters-above-content-ctn { padding: 0.4rem 0.9rem !important; }
.fi-ta-filters-heading { display: none !important; }
+12
View File
@@ -33,6 +33,18 @@ if (! function_exists('theme')) {
}
}
if (! function_exists('blog_paginate')) {
/**
* Front-end pager: fewer page chips so the sidebar column does not wrap under.
*/
function blog_paginate(\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $query, ?int $perPage = null): \Illuminate\Contracts\Pagination\LengthAwarePaginator
{
$perPage = max(1, (int) ($perPage ?? blog_setting('per_page', config('blog.per_page'))));
return $query->paginate($perPage)->onEachSide(1)->withQueryString();
}
}
if (! function_exists('blog_setting')) {
/**
* 读取博客设置(Setting 表,带缓存)。
+2
View File
@@ -25,6 +25,8 @@ return [
'per_page' => 10,
'home_view_mode' => 'list',
'rss_num' => 20,
'comment_min_len' => 2,
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (! Schema::hasColumn('posts', 'sticky_level')) {
Schema::table('posts', function (Blueprint $table) {
$table->unsignedTinyInteger('sticky_level')->default(0)->after('is_sticky');
});
}
DB::table('posts')->where('is_sticky', true)->where('sticky_level', 0)->update(['sticky_level' => 1]);
}
public function down(): void
{
if (Schema::hasColumn('posts', 'sticky_level')) {
Schema::table('posts', function (Blueprint $table) {
$table->dropColumn('sticky_level');
});
}
}
};
+2
View File
@@ -392,6 +392,7 @@ CREATE TABLE `posts` (
`cover_media_id` bigint unsigned DEFAULT NULL,
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'published',
`is_sticky` tinyint(1) NOT NULL DEFAULT '0',
`sticky_level` tinyint unsigned NOT NULL DEFAULT '0',
`close_comment` tinyint(1) NOT NULL DEFAULT '0',
`read_password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`views` bigint unsigned NOT NULL DEFAULT '0',
@@ -580,3 +581,4 @@ INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (22,'2026_08_15_000
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (23,'2026_08_15_000004_add_custom_properties_to_media_library_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (24,'2026_09_16_000001_add_cover_media_id_to_posts_and_categories',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (25,'2026_09_16_000002_seed_default_media_folders',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (26,'2026_09_18_210000_add_sticky_level_to_posts_table',3);
+1
View File
@@ -33,6 +33,7 @@ class DatabaseSeeder extends Seeder
'site_description' => config('blog.description'),
'site_icp' => config('blog.icp'),
'active_theme' => 'sablog',
'home_view_mode' => 'list',
'comment_audit' => '0',
'comment_min_len' => (string) config('blog.comment_min_len'),
'comment_max_len' => (string) config('blog.comment_max_len'),
+1 -1
View File
@@ -27,7 +27,7 @@ app/
Providers/ # ThemeServiceProvider、PluginManagerServiceProvider
View/Composers/ # 侧边栏数据共享
Filament/ # 后台(Resources/Pages/Widgets)
Console/Commands/ # sablog:import / attachments:sync-s3 / workerman:serve / content:convert / theme:publish
Console/Commands/ # sablog:import / sablog:fix-content / attachments:sync-s3 / workerman:serve / content:convert / theme:publish
Models/ # Post/Category/Comment/Link/Setting/User...
themes/ # 皮肤(theme.json + views + assets)
plugins/ # 插件(plugin.json + ServiceProvider + 迁移/路由/视图)
@@ -0,0 +1,582 @@
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
DROP TABLE IF EXISTS `cache`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `cache` (
`key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`value` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
`expiration` int NOT NULL,
PRIMARY KEY (`key`),
KEY `cache_expiration_index` (`expiration`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `cache_locks`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `cache_locks` (
`key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`owner` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`expiration` int NOT NULL,
PRIMARY KEY (`key`),
KEY `cache_locks_expiration_index` (`expiration`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `categories`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `categories` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`slug` varchar(120) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`display_order` tinyint NOT NULL DEFAULT '0',
`description` text COLLATE utf8mb4_unicode_ci,
`cover_media_id` bigint unsigned DEFAULT NULL,
`post_count` int unsigned NOT NULL DEFAULT '0',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `categories_slug_unique` (`slug`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `comments`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `comments` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`post_id` bigint unsigned NOT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`author_name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`author_email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`author_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`content` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
`ip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
`ai_review` json DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `comments_user_id_foreign` (`user_id`),
KEY `comments_post_id_status_index` (`post_id`,`status`),
KEY `comments_status_index` (`status`),
CONSTRAINT `comments_post_id_foreign` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE,
CONSTRAINT `comments_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `failed_jobs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `failed_jobs` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`uuid` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`connection` text COLLATE utf8mb4_unicode_ci NOT NULL,
`queue` text COLLATE utf8mb4_unicode_ci NOT NULL,
`payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`exception` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`failed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `failed_jobs_uuid_unique` (`uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `hello_world_logs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `hello_world_logs` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`comment_id` bigint unsigned DEFAULT NULL,
`note` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `hello_world_logs_comment_id_foreign` (`comment_id`),
CONSTRAINT `hello_world_logs_comment_id_foreign` FOREIGN KEY (`comment_id`) REFERENCES `comments` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `job_batches`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `job_batches` (
`id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`total_jobs` int NOT NULL,
`pending_jobs` int NOT NULL,
`failed_jobs` int NOT NULL,
`failed_job_ids` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`options` mediumtext COLLATE utf8mb4_unicode_ci,
`cancelled_at` int DEFAULT NULL,
`created_at` int NOT NULL,
`finished_at` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `jobs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `jobs` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`queue` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`attempts` tinyint unsigned NOT NULL,
`reserved_at` int unsigned DEFAULT NULL,
`available_at` int unsigned NOT NULL,
`created_at` int unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `jobs_queue_index` (`queue`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `links`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `links` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`url` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`note` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`display_order` tinyint NOT NULL DEFAULT '0',
`visible` tinyint(1) NOT NULL DEFAULT '1',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `market_packages`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `market_packages` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`item_key` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
`item_type` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
`title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`price` bigint unsigned NOT NULL,
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
`payment_id` bigint unsigned DEFAULT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `market_packages_item_key_item_type_unique` (`item_key`,`item_type`),
KEY `market_packages_payment_id_foreign` (`payment_id`),
KEY `market_packages_user_id_foreign` (`user_id`),
CONSTRAINT `market_packages_payment_id_foreign` FOREIGN KEY (`payment_id`) REFERENCES `payments` (`id`) ON DELETE SET NULL,
CONSTRAINT `market_packages_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `media`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `media` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`model_id` bigint unsigned NOT NULL,
`uuid` char(36) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`collection_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`file_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`mime_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`disk` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`conversions_disk` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`size` bigint unsigned NOT NULL,
`manipulations` json NOT NULL,
`custom_properties` json NOT NULL,
`generated_conversions` json NOT NULL,
`responsive_images` json NOT NULL,
`order_column` int unsigned DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `media_uuid_unique` (`uuid`),
KEY `media_model_type_model_id_index` (`model_type`,`model_id`),
KEY `media_order_column_index` (`order_column`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `media_folders`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `media_folders` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`parent_id` bigint unsigned DEFAULT NULL,
`code` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`sort` int NOT NULL DEFAULT '0',
`is_active` tinyint(1) NOT NULL DEFAULT '1',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `media_folders_code_unique` (`code`),
KEY `media_folders_parent_id_foreign` (`parent_id`),
CONSTRAINT `media_folders_parent_id_foreign` FOREIGN KEY (`parent_id`) REFERENCES `media_folders` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `media_library`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `media_library` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint unsigned DEFAULT NULL,
`disk` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'public',
`path` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`original_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`extension` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`mime_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`size` bigint unsigned NOT NULL DEFAULT '0',
`type` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'image',
`source` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'admin',
`folder` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`alt_text` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`custom_properties` json DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`deleted_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `media_library_path_unique` (`path`),
KEY `media_library_user_id_foreign` (`user_id`),
KEY `media_library_type_index` (`type`),
KEY `media_library_folder_index` (`folder`),
KEY `media_library_source_index` (`source`),
CONSTRAINT `media_library_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `membership_plans`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `membership_plans` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
`slug` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
`description` text COLLATE utf8mb4_unicode_ci,
`price` bigint unsigned NOT NULL,
`duration_days` int unsigned NOT NULL DEFAULT '30',
`permissions` json DEFAULT NULL,
`active` tinyint(1) NOT NULL DEFAULT '1',
`sort` int unsigned NOT NULL DEFAULT '0',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `membership_plans_slug_unique` (`slug`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `migrations`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `migrations` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`migration` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`batch` int NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `model_has_permissions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `model_has_permissions` (
`permission_id` bigint unsigned NOT NULL,
`model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`model_id` bigint unsigned NOT NULL,
PRIMARY KEY (`permission_id`,`model_id`,`model_type`),
KEY `model_has_permissions_model_id_model_type_index` (`model_id`,`model_type`),
CONSTRAINT `model_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `model_has_roles`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `model_has_roles` (
`role_id` bigint unsigned NOT NULL,
`model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`model_id` bigint unsigned NOT NULL,
PRIMARY KEY (`role_id`,`model_id`,`model_type`),
KEY `model_has_roles_model_id_model_type_index` (`model_id`,`model_type`),
CONSTRAINT `model_has_roles_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `password_reset_tokens`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `password_reset_tokens` (
`email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`token` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `payments`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `payments` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`order_no` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`subject` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`description` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`amount` bigint unsigned NOT NULL,
`channel` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'alipay',
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
`gateway_trade_no` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`payload` json DEFAULT NULL,
`paid_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`payable_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`payable_id` bigint unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `payments_order_no_unique` (`order_no`),
KEY `payments_user_id_status_index` (`user_id`,`status`),
KEY `payments_payable_type_payable_id_index` (`payable_type`,`payable_id`),
CONSTRAINT `payments_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `permissions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `permissions` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`guard_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `permissions_name_guard_name_unique` (`name`,`guard_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `personal_access_tokens`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `personal_access_tokens` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`tokenable_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`tokenable_id` bigint unsigned NOT NULL,
`name` text COLLATE utf8mb4_unicode_ci NOT NULL,
`token` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
`abilities` text COLLATE utf8mb4_unicode_ci,
`last_used_at` timestamp NULL DEFAULT NULL,
`expires_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `personal_access_tokens_token_unique` (`token`),
KEY `personal_access_tokens_tokenable_type_tokenable_id_index` (`tokenable_type`,`tokenable_id`),
KEY `personal_access_tokens_expires_at_index` (`expires_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `plugin_records`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `plugin_records` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`vendor` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
`version` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`enabled` tinyint(1) NOT NULL DEFAULT '1',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `plugin_records_vendor_name_unique` (`vendor`,`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `posts`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `posts` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`category_id` bigint unsigned DEFAULT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`slug` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`excerpt` text COLLATE utf8mb4_unicode_ci,
`content` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`content_format` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'markdown',
`keywords` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`cover_media_id` bigint unsigned DEFAULT NULL,
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'published',
`is_sticky` tinyint(1) NOT NULL DEFAULT '0',
`close_comment` tinyint(1) NOT NULL DEFAULT '0',
`read_password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`views` bigint unsigned NOT NULL DEFAULT '0',
`comment_count` int unsigned NOT NULL DEFAULT '0',
`meta` json DEFAULT NULL,
`published_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `posts_slug_unique` (`slug`),
KEY `posts_category_id_foreign` (`category_id`),
KEY `posts_user_id_foreign` (`user_id`),
KEY `posts_status_published_at_index` (`status`,`published_at`),
KEY `posts_status_index` (`status`),
KEY `posts_published_at_index` (`published_at`),
CONSTRAINT `posts_category_id_foreign` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL,
CONSTRAINT `posts_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `redirects`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `redirects` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`hash` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL,
`target_url` text COLLATE utf8mb4_unicode_ci NOT NULL,
`clicks` bigint unsigned NOT NULL DEFAULT '0',
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active',
`last_clicked_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `redirects_hash_unique` (`hash`),
KEY `redirects_target_url_index` (`target_url`(191))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `role_has_permissions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `role_has_permissions` (
`permission_id` bigint unsigned NOT NULL,
`role_id` bigint unsigned NOT NULL,
PRIMARY KEY (`permission_id`,`role_id`),
KEY `role_has_permissions_role_id_foreign` (`role_id`),
CONSTRAINT `role_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE,
CONSTRAINT `role_has_permissions_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `roles`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `roles` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`guard_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `roles_name_guard_name_unique` (`name`,`guard_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `sessions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `sessions` (
`id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`ip_address` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`user_agent` text COLLATE utf8mb4_unicode_ci,
`payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`last_activity` int NOT NULL,
PRIMARY KEY (`id`),
KEY `sessions_user_id_index` (`user_id`),
KEY `sessions_last_activity_index` (`last_activity`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `settings`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `settings` (
`key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`value` text COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `subscriptions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `subscriptions` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint unsigned NOT NULL,
`membership_plan_id` bigint unsigned DEFAULT NULL,
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active',
`starts_at` timestamp NULL DEFAULT NULL,
`ends_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `subscriptions_membership_plan_id_foreign` (`membership_plan_id`),
KEY `subscriptions_user_id_status_index` (`user_id`,`status`),
CONSTRAINT `subscriptions_membership_plan_id_foreign` FOREIGN KEY (`membership_plan_id`) REFERENCES `membership_plans` (`id`) ON DELETE SET NULL,
CONSTRAINT `subscriptions_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `taggables`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `taggables` (
`tag_id` bigint unsigned NOT NULL,
`taggable_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`taggable_id` bigint unsigned NOT NULL,
UNIQUE KEY `taggables_tag_id_taggable_id_taggable_type_unique` (`tag_id`,`taggable_id`,`taggable_type`),
KEY `taggables_taggable_type_taggable_id_index` (`taggable_type`,`taggable_id`),
CONSTRAINT `taggables_tag_id_foreign` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `tags`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `tags` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` json NOT NULL,
`slug` json NOT NULL,
`type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`order_column` int DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `users` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`email_verified_at` timestamp NULL DEFAULT NULL,
`password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`legacy_md5` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`logincount` int unsigned NOT NULL DEFAULT '0',
`loginip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`regip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`logintime` timestamp NULL DEFAULT NULL,
`lastpost_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `users_email_unique` (`email`),
KEY `users_legacy_md5_index` (`legacy_md5`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (1,'0001_01_01_000000_create_users_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (2,'0001_01_01_000001_create_cache_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (3,'0001_01_01_000002_create_jobs_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (4,'2026_08_11_092446_create_permission_tables',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (5,'2026_08_11_092448_create_media_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (6,'2026_08_11_092449_create_tag_tables',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (7,'2026_08_11_100000_create_membership_tables',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (8,'2026_08_11_100000_create_payments_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (9,'2026_08_11_100001_create_categories_and_posts_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (10,'2026_08_11_100002_create_comments_and_trackbacks_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (11,'2026_08_11_100003_create_links_and_settings_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (12,'2026_08_11_100004_add_blog_columns_to_users_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (13,'2026_08_11_100005_add_content_format_to_posts_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (14,'2026_08_11_113537_create_personal_access_tokens_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (15,'2026_08_12_000000_create_hello_world_logs_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (16,'2026_08_12_100000_add_payable_to_payments_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (17,'2026_08_12_200000_create_market_packages_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (18,'2026_08_12_300000_add_description_to_categories_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (19,'2026_08_12_400000_create_redirects_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (20,'2026_08_15_000001_create_media_library_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (21,'2026_08_15_000002_create_media_folders_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (22,'2026_08_15_000003_add_soft_deletes_to_media_library_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (23,'2026_08_15_000004_add_custom_properties_to_media_library_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (24,'2026_09_16_000001_add_cover_media_id_to_posts_and_categories',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (25,'2026_09_16_000002_seed_default_media_folders',2);
@@ -0,0 +1,584 @@
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
DROP TABLE IF EXISTS `cache`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `cache` (
`key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`value` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
`expiration` int NOT NULL,
PRIMARY KEY (`key`),
KEY `cache_expiration_index` (`expiration`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `cache_locks`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `cache_locks` (
`key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`owner` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`expiration` int NOT NULL,
PRIMARY KEY (`key`),
KEY `cache_locks_expiration_index` (`expiration`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `categories`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `categories` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`slug` varchar(120) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`display_order` tinyint NOT NULL DEFAULT '0',
`description` text COLLATE utf8mb4_unicode_ci,
`cover_media_id` bigint unsigned DEFAULT NULL,
`post_count` int unsigned NOT NULL DEFAULT '0',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `categories_slug_unique` (`slug`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `comments`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `comments` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`post_id` bigint unsigned NOT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`author_name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`author_email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`author_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`content` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
`ip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
`ai_review` json DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `comments_user_id_foreign` (`user_id`),
KEY `comments_post_id_status_index` (`post_id`,`status`),
KEY `comments_status_index` (`status`),
CONSTRAINT `comments_post_id_foreign` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE,
CONSTRAINT `comments_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `failed_jobs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `failed_jobs` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`uuid` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`connection` text COLLATE utf8mb4_unicode_ci NOT NULL,
`queue` text COLLATE utf8mb4_unicode_ci NOT NULL,
`payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`exception` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`failed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `failed_jobs_uuid_unique` (`uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `hello_world_logs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `hello_world_logs` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`comment_id` bigint unsigned DEFAULT NULL,
`note` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `hello_world_logs_comment_id_foreign` (`comment_id`),
CONSTRAINT `hello_world_logs_comment_id_foreign` FOREIGN KEY (`comment_id`) REFERENCES `comments` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `job_batches`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `job_batches` (
`id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`total_jobs` int NOT NULL,
`pending_jobs` int NOT NULL,
`failed_jobs` int NOT NULL,
`failed_job_ids` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`options` mediumtext COLLATE utf8mb4_unicode_ci,
`cancelled_at` int DEFAULT NULL,
`created_at` int NOT NULL,
`finished_at` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `jobs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `jobs` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`queue` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`attempts` tinyint unsigned NOT NULL,
`reserved_at` int unsigned DEFAULT NULL,
`available_at` int unsigned NOT NULL,
`created_at` int unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `jobs_queue_index` (`queue`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `links`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `links` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`url` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`note` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`display_order` tinyint NOT NULL DEFAULT '0',
`visible` tinyint(1) NOT NULL DEFAULT '1',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `market_packages`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `market_packages` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`item_key` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
`item_type` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
`title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`price` bigint unsigned NOT NULL,
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
`payment_id` bigint unsigned DEFAULT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `market_packages_item_key_item_type_unique` (`item_key`,`item_type`),
KEY `market_packages_payment_id_foreign` (`payment_id`),
KEY `market_packages_user_id_foreign` (`user_id`),
CONSTRAINT `market_packages_payment_id_foreign` FOREIGN KEY (`payment_id`) REFERENCES `payments` (`id`) ON DELETE SET NULL,
CONSTRAINT `market_packages_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `media`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `media` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`model_id` bigint unsigned NOT NULL,
`uuid` char(36) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`collection_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`file_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`mime_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`disk` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`conversions_disk` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`size` bigint unsigned NOT NULL,
`manipulations` json NOT NULL,
`custom_properties` json NOT NULL,
`generated_conversions` json NOT NULL,
`responsive_images` json NOT NULL,
`order_column` int unsigned DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `media_uuid_unique` (`uuid`),
KEY `media_model_type_model_id_index` (`model_type`,`model_id`),
KEY `media_order_column_index` (`order_column`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `media_folders`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `media_folders` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`parent_id` bigint unsigned DEFAULT NULL,
`code` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`sort` int NOT NULL DEFAULT '0',
`is_active` tinyint(1) NOT NULL DEFAULT '1',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `media_folders_code_unique` (`code`),
KEY `media_folders_parent_id_foreign` (`parent_id`),
CONSTRAINT `media_folders_parent_id_foreign` FOREIGN KEY (`parent_id`) REFERENCES `media_folders` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `media_library`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `media_library` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint unsigned DEFAULT NULL,
`disk` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'public',
`path` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`original_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`extension` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`mime_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`size` bigint unsigned NOT NULL DEFAULT '0',
`type` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'image',
`source` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'admin',
`folder` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`alt_text` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`custom_properties` json DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`deleted_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `media_library_path_unique` (`path`),
KEY `media_library_user_id_foreign` (`user_id`),
KEY `media_library_type_index` (`type`),
KEY `media_library_folder_index` (`folder`),
KEY `media_library_source_index` (`source`),
CONSTRAINT `media_library_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `membership_plans`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `membership_plans` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
`slug` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
`description` text COLLATE utf8mb4_unicode_ci,
`price` bigint unsigned NOT NULL,
`duration_days` int unsigned NOT NULL DEFAULT '30',
`permissions` json DEFAULT NULL,
`active` tinyint(1) NOT NULL DEFAULT '1',
`sort` int unsigned NOT NULL DEFAULT '0',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `membership_plans_slug_unique` (`slug`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `migrations`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `migrations` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`migration` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`batch` int NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `model_has_permissions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `model_has_permissions` (
`permission_id` bigint unsigned NOT NULL,
`model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`model_id` bigint unsigned NOT NULL,
PRIMARY KEY (`permission_id`,`model_id`,`model_type`),
KEY `model_has_permissions_model_id_model_type_index` (`model_id`,`model_type`),
CONSTRAINT `model_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `model_has_roles`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `model_has_roles` (
`role_id` bigint unsigned NOT NULL,
`model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`model_id` bigint unsigned NOT NULL,
PRIMARY KEY (`role_id`,`model_id`,`model_type`),
KEY `model_has_roles_model_id_model_type_index` (`model_id`,`model_type`),
CONSTRAINT `model_has_roles_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `password_reset_tokens`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `password_reset_tokens` (
`email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`token` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `payments`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `payments` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`order_no` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`subject` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`description` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`amount` bigint unsigned NOT NULL,
`channel` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'alipay',
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
`gateway_trade_no` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`payload` json DEFAULT NULL,
`paid_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`payable_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`payable_id` bigint unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `payments_order_no_unique` (`order_no`),
KEY `payments_user_id_status_index` (`user_id`,`status`),
KEY `payments_payable_type_payable_id_index` (`payable_type`,`payable_id`),
CONSTRAINT `payments_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `permissions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `permissions` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`guard_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `permissions_name_guard_name_unique` (`name`,`guard_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `personal_access_tokens`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `personal_access_tokens` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`tokenable_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`tokenable_id` bigint unsigned NOT NULL,
`name` text COLLATE utf8mb4_unicode_ci NOT NULL,
`token` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
`abilities` text COLLATE utf8mb4_unicode_ci,
`last_used_at` timestamp NULL DEFAULT NULL,
`expires_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `personal_access_tokens_token_unique` (`token`),
KEY `personal_access_tokens_tokenable_type_tokenable_id_index` (`tokenable_type`,`tokenable_id`),
KEY `personal_access_tokens_expires_at_index` (`expires_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `plugin_records`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `plugin_records` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`vendor` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
`version` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`enabled` tinyint(1) NOT NULL DEFAULT '1',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `plugin_records_vendor_name_unique` (`vendor`,`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `posts`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `posts` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`category_id` bigint unsigned DEFAULT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`slug` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`excerpt` text COLLATE utf8mb4_unicode_ci,
`content` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`content_format` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'markdown',
`keywords` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`cover_media_id` bigint unsigned DEFAULT NULL,
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'published',
`is_sticky` tinyint(1) NOT NULL DEFAULT '0',
`sticky_level` tinyint unsigned NOT NULL DEFAULT '0',
`close_comment` tinyint(1) NOT NULL DEFAULT '0',
`read_password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`views` bigint unsigned NOT NULL DEFAULT '0',
`comment_count` int unsigned NOT NULL DEFAULT '0',
`meta` json DEFAULT NULL,
`published_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `posts_slug_unique` (`slug`),
KEY `posts_category_id_foreign` (`category_id`),
KEY `posts_user_id_foreign` (`user_id`),
KEY `posts_status_published_at_index` (`status`,`published_at`),
KEY `posts_status_index` (`status`),
KEY `posts_published_at_index` (`published_at`),
CONSTRAINT `posts_category_id_foreign` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL,
CONSTRAINT `posts_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `redirects`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `redirects` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`hash` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL,
`target_url` text COLLATE utf8mb4_unicode_ci NOT NULL,
`clicks` bigint unsigned NOT NULL DEFAULT '0',
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active',
`last_clicked_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `redirects_hash_unique` (`hash`),
KEY `redirects_target_url_index` (`target_url`(191))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `role_has_permissions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `role_has_permissions` (
`permission_id` bigint unsigned NOT NULL,
`role_id` bigint unsigned NOT NULL,
PRIMARY KEY (`permission_id`,`role_id`),
KEY `role_has_permissions_role_id_foreign` (`role_id`),
CONSTRAINT `role_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE,
CONSTRAINT `role_has_permissions_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `roles`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `roles` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`guard_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `roles_name_guard_name_unique` (`name`,`guard_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `sessions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `sessions` (
`id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`ip_address` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`user_agent` text COLLATE utf8mb4_unicode_ci,
`payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`last_activity` int NOT NULL,
PRIMARY KEY (`id`),
KEY `sessions_user_id_index` (`user_id`),
KEY `sessions_last_activity_index` (`last_activity`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `settings`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `settings` (
`key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`value` text COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `subscriptions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `subscriptions` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint unsigned NOT NULL,
`membership_plan_id` bigint unsigned DEFAULT NULL,
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active',
`starts_at` timestamp NULL DEFAULT NULL,
`ends_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `subscriptions_membership_plan_id_foreign` (`membership_plan_id`),
KEY `subscriptions_user_id_status_index` (`user_id`,`status`),
CONSTRAINT `subscriptions_membership_plan_id_foreign` FOREIGN KEY (`membership_plan_id`) REFERENCES `membership_plans` (`id`) ON DELETE SET NULL,
CONSTRAINT `subscriptions_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `taggables`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `taggables` (
`tag_id` bigint unsigned NOT NULL,
`taggable_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`taggable_id` bigint unsigned NOT NULL,
UNIQUE KEY `taggables_tag_id_taggable_id_taggable_type_unique` (`tag_id`,`taggable_id`,`taggable_type`),
KEY `taggables_taggable_type_taggable_id_index` (`taggable_type`,`taggable_id`),
CONSTRAINT `taggables_tag_id_foreign` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `tags`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `tags` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` json NOT NULL,
`slug` json NOT NULL,
`type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`order_column` int DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `users` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`email_verified_at` timestamp NULL DEFAULT NULL,
`password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`legacy_md5` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`logincount` int unsigned NOT NULL DEFAULT '0',
`loginip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`regip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`logintime` timestamp NULL DEFAULT NULL,
`lastpost_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `users_email_unique` (`email`),
KEY `users_legacy_md5_index` (`legacy_md5`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (1,'0001_01_01_000000_create_users_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (2,'0001_01_01_000001_create_cache_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (3,'0001_01_01_000002_create_jobs_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (4,'2026_08_11_092446_create_permission_tables',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (5,'2026_08_11_092448_create_media_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (6,'2026_08_11_092449_create_tag_tables',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (7,'2026_08_11_100000_create_membership_tables',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (8,'2026_08_11_100000_create_payments_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (9,'2026_08_11_100001_create_categories_and_posts_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (10,'2026_08_11_100002_create_comments_and_trackbacks_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (11,'2026_08_11_100003_create_links_and_settings_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (12,'2026_08_11_100004_add_blog_columns_to_users_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (13,'2026_08_11_100005_add_content_format_to_posts_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (14,'2026_08_11_113537_create_personal_access_tokens_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (15,'2026_08_12_000000_create_hello_world_logs_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (16,'2026_08_12_100000_add_payable_to_payments_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (17,'2026_08_12_200000_create_market_packages_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (18,'2026_08_12_300000_add_description_to_categories_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (19,'2026_08_12_400000_create_redirects_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (20,'2026_08_15_000001_create_media_library_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (21,'2026_08_15_000002_create_media_folders_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (22,'2026_08_15_000003_add_soft_deletes_to_media_library_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (23,'2026_08_15_000004_add_custom_properties_to_media_library_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (24,'2026_09_16_000001_add_cover_media_id_to_posts_and_categories',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (25,'2026_09_16_000002_seed_default_media_folders',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (26,'2026_09_18_210000_add_sticky_level_to_posts_table',3);
@@ -0,0 +1,584 @@
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
DROP TABLE IF EXISTS `cache`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `cache` (
`key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`value` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
`expiration` int NOT NULL,
PRIMARY KEY (`key`),
KEY `cache_expiration_index` (`expiration`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `cache_locks`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `cache_locks` (
`key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`owner` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`expiration` int NOT NULL,
PRIMARY KEY (`key`),
KEY `cache_locks_expiration_index` (`expiration`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `categories`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `categories` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`slug` varchar(120) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`display_order` tinyint NOT NULL DEFAULT '0',
`description` text COLLATE utf8mb4_unicode_ci,
`cover_media_id` bigint unsigned DEFAULT NULL,
`post_count` int unsigned NOT NULL DEFAULT '0',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `categories_slug_unique` (`slug`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `comments`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `comments` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`post_id` bigint unsigned NOT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`author_name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`author_email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`author_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`content` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
`ip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
`ai_review` json DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `comments_user_id_foreign` (`user_id`),
KEY `comments_post_id_status_index` (`post_id`,`status`),
KEY `comments_status_index` (`status`),
CONSTRAINT `comments_post_id_foreign` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE,
CONSTRAINT `comments_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `failed_jobs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `failed_jobs` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`uuid` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`connection` text COLLATE utf8mb4_unicode_ci NOT NULL,
`queue` text COLLATE utf8mb4_unicode_ci NOT NULL,
`payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`exception` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`failed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `failed_jobs_uuid_unique` (`uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `hello_world_logs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `hello_world_logs` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`comment_id` bigint unsigned DEFAULT NULL,
`note` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `hello_world_logs_comment_id_foreign` (`comment_id`),
CONSTRAINT `hello_world_logs_comment_id_foreign` FOREIGN KEY (`comment_id`) REFERENCES `comments` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `job_batches`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `job_batches` (
`id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`total_jobs` int NOT NULL,
`pending_jobs` int NOT NULL,
`failed_jobs` int NOT NULL,
`failed_job_ids` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`options` mediumtext COLLATE utf8mb4_unicode_ci,
`cancelled_at` int DEFAULT NULL,
`created_at` int NOT NULL,
`finished_at` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `jobs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `jobs` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`queue` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`attempts` tinyint unsigned NOT NULL,
`reserved_at` int unsigned DEFAULT NULL,
`available_at` int unsigned NOT NULL,
`created_at` int unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `jobs_queue_index` (`queue`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `links`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `links` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`url` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`note` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`display_order` tinyint NOT NULL DEFAULT '0',
`visible` tinyint(1) NOT NULL DEFAULT '1',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `market_packages`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `market_packages` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`item_key` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
`item_type` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
`title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`price` bigint unsigned NOT NULL,
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
`payment_id` bigint unsigned DEFAULT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `market_packages_item_key_item_type_unique` (`item_key`,`item_type`),
KEY `market_packages_payment_id_foreign` (`payment_id`),
KEY `market_packages_user_id_foreign` (`user_id`),
CONSTRAINT `market_packages_payment_id_foreign` FOREIGN KEY (`payment_id`) REFERENCES `payments` (`id`) ON DELETE SET NULL,
CONSTRAINT `market_packages_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `media`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `media` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`model_id` bigint unsigned NOT NULL,
`uuid` char(36) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`collection_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`file_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`mime_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`disk` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`conversions_disk` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`size` bigint unsigned NOT NULL,
`manipulations` json NOT NULL,
`custom_properties` json NOT NULL,
`generated_conversions` json NOT NULL,
`responsive_images` json NOT NULL,
`order_column` int unsigned DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `media_uuid_unique` (`uuid`),
KEY `media_model_type_model_id_index` (`model_type`,`model_id`),
KEY `media_order_column_index` (`order_column`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `media_folders`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `media_folders` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`parent_id` bigint unsigned DEFAULT NULL,
`code` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`sort` int NOT NULL DEFAULT '0',
`is_active` tinyint(1) NOT NULL DEFAULT '1',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `media_folders_code_unique` (`code`),
KEY `media_folders_parent_id_foreign` (`parent_id`),
CONSTRAINT `media_folders_parent_id_foreign` FOREIGN KEY (`parent_id`) REFERENCES `media_folders` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `media_library`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `media_library` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint unsigned DEFAULT NULL,
`disk` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'public',
`path` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`original_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`extension` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`mime_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`size` bigint unsigned NOT NULL DEFAULT '0',
`type` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'image',
`source` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'admin',
`folder` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`alt_text` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`custom_properties` json DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`deleted_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `media_library_path_unique` (`path`),
KEY `media_library_user_id_foreign` (`user_id`),
KEY `media_library_type_index` (`type`),
KEY `media_library_folder_index` (`folder`),
KEY `media_library_source_index` (`source`),
CONSTRAINT `media_library_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `membership_plans`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `membership_plans` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
`slug` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
`description` text COLLATE utf8mb4_unicode_ci,
`price` bigint unsigned NOT NULL,
`duration_days` int unsigned NOT NULL DEFAULT '30',
`permissions` json DEFAULT NULL,
`active` tinyint(1) NOT NULL DEFAULT '1',
`sort` int unsigned NOT NULL DEFAULT '0',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `membership_plans_slug_unique` (`slug`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `migrations`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `migrations` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`migration` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`batch` int NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `model_has_permissions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `model_has_permissions` (
`permission_id` bigint unsigned NOT NULL,
`model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`model_id` bigint unsigned NOT NULL,
PRIMARY KEY (`permission_id`,`model_id`,`model_type`),
KEY `model_has_permissions_model_id_model_type_index` (`model_id`,`model_type`),
CONSTRAINT `model_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `model_has_roles`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `model_has_roles` (
`role_id` bigint unsigned NOT NULL,
`model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`model_id` bigint unsigned NOT NULL,
PRIMARY KEY (`role_id`,`model_id`,`model_type`),
KEY `model_has_roles_model_id_model_type_index` (`model_id`,`model_type`),
CONSTRAINT `model_has_roles_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `password_reset_tokens`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `password_reset_tokens` (
`email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`token` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `payments`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `payments` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`order_no` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`subject` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`description` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`amount` bigint unsigned NOT NULL,
`channel` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'alipay',
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
`gateway_trade_no` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`payload` json DEFAULT NULL,
`paid_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`payable_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`payable_id` bigint unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `payments_order_no_unique` (`order_no`),
KEY `payments_user_id_status_index` (`user_id`,`status`),
KEY `payments_payable_type_payable_id_index` (`payable_type`,`payable_id`),
CONSTRAINT `payments_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `permissions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `permissions` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`guard_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `permissions_name_guard_name_unique` (`name`,`guard_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `personal_access_tokens`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `personal_access_tokens` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`tokenable_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`tokenable_id` bigint unsigned NOT NULL,
`name` text COLLATE utf8mb4_unicode_ci NOT NULL,
`token` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
`abilities` text COLLATE utf8mb4_unicode_ci,
`last_used_at` timestamp NULL DEFAULT NULL,
`expires_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `personal_access_tokens_token_unique` (`token`),
KEY `personal_access_tokens_tokenable_type_tokenable_id_index` (`tokenable_type`,`tokenable_id`),
KEY `personal_access_tokens_expires_at_index` (`expires_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `plugin_records`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `plugin_records` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`vendor` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
`version` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`enabled` tinyint(1) NOT NULL DEFAULT '1',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `plugin_records_vendor_name_unique` (`vendor`,`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `posts`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `posts` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`category_id` bigint unsigned DEFAULT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`slug` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`excerpt` text COLLATE utf8mb4_unicode_ci,
`content` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`content_format` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'markdown',
`keywords` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`cover_media_id` bigint unsigned DEFAULT NULL,
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'published',
`is_sticky` tinyint(1) NOT NULL DEFAULT '0',
`sticky_level` tinyint unsigned NOT NULL DEFAULT '0',
`close_comment` tinyint(1) NOT NULL DEFAULT '0',
`read_password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`views` bigint unsigned NOT NULL DEFAULT '0',
`comment_count` int unsigned NOT NULL DEFAULT '0',
`meta` json DEFAULT NULL,
`published_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `posts_slug_unique` (`slug`),
KEY `posts_category_id_foreign` (`category_id`),
KEY `posts_user_id_foreign` (`user_id`),
KEY `posts_status_published_at_index` (`status`,`published_at`),
KEY `posts_status_index` (`status`),
KEY `posts_published_at_index` (`published_at`),
CONSTRAINT `posts_category_id_foreign` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL,
CONSTRAINT `posts_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `redirects`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `redirects` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`hash` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL,
`target_url` text COLLATE utf8mb4_unicode_ci NOT NULL,
`clicks` bigint unsigned NOT NULL DEFAULT '0',
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active',
`last_clicked_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `redirects_hash_unique` (`hash`),
KEY `redirects_target_url_index` (`target_url`(191))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `role_has_permissions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `role_has_permissions` (
`permission_id` bigint unsigned NOT NULL,
`role_id` bigint unsigned NOT NULL,
PRIMARY KEY (`permission_id`,`role_id`),
KEY `role_has_permissions_role_id_foreign` (`role_id`),
CONSTRAINT `role_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE,
CONSTRAINT `role_has_permissions_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `roles`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `roles` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`guard_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `roles_name_guard_name_unique` (`name`,`guard_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `sessions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `sessions` (
`id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`ip_address` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`user_agent` text COLLATE utf8mb4_unicode_ci,
`payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`last_activity` int NOT NULL,
PRIMARY KEY (`id`),
KEY `sessions_user_id_index` (`user_id`),
KEY `sessions_last_activity_index` (`last_activity`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `settings`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `settings` (
`key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`value` text COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `subscriptions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `subscriptions` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint unsigned NOT NULL,
`membership_plan_id` bigint unsigned DEFAULT NULL,
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active',
`starts_at` timestamp NULL DEFAULT NULL,
`ends_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `subscriptions_membership_plan_id_foreign` (`membership_plan_id`),
KEY `subscriptions_user_id_status_index` (`user_id`,`status`),
CONSTRAINT `subscriptions_membership_plan_id_foreign` FOREIGN KEY (`membership_plan_id`) REFERENCES `membership_plans` (`id`) ON DELETE SET NULL,
CONSTRAINT `subscriptions_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `taggables`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `taggables` (
`tag_id` bigint unsigned NOT NULL,
`taggable_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`taggable_id` bigint unsigned NOT NULL,
UNIQUE KEY `taggables_tag_id_taggable_id_taggable_type_unique` (`tag_id`,`taggable_id`,`taggable_type`),
KEY `taggables_taggable_type_taggable_id_index` (`taggable_type`,`taggable_id`),
CONSTRAINT `taggables_tag_id_foreign` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `tags`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `tags` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` json NOT NULL,
`slug` json NOT NULL,
`type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`order_column` int DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `users` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`email_verified_at` timestamp NULL DEFAULT NULL,
`password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`legacy_md5` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`logincount` int unsigned NOT NULL DEFAULT '0',
`loginip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`regip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`logintime` timestamp NULL DEFAULT NULL,
`lastpost_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `users_email_unique` (`email`),
KEY `users_legacy_md5_index` (`legacy_md5`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (1,'0001_01_01_000000_create_users_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (2,'0001_01_01_000001_create_cache_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (3,'0001_01_01_000002_create_jobs_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (4,'2026_08_11_092446_create_permission_tables',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (5,'2026_08_11_092448_create_media_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (6,'2026_08_11_092449_create_tag_tables',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (7,'2026_08_11_100000_create_membership_tables',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (8,'2026_08_11_100000_create_payments_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (9,'2026_08_11_100001_create_categories_and_posts_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (10,'2026_08_11_100002_create_comments_and_trackbacks_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (11,'2026_08_11_100003_create_links_and_settings_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (12,'2026_08_11_100004_add_blog_columns_to_users_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (13,'2026_08_11_100005_add_content_format_to_posts_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (14,'2026_08_11_113537_create_personal_access_tokens_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (15,'2026_08_12_000000_create_hello_world_logs_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (16,'2026_08_12_100000_add_payable_to_payments_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (17,'2026_08_12_200000_create_market_packages_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (18,'2026_08_12_300000_add_description_to_categories_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (19,'2026_08_12_400000_create_redirects_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (20,'2026_08_15_000001_create_media_library_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (21,'2026_08_15_000002_create_media_folders_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (22,'2026_08_15_000003_add_soft_deletes_to_media_library_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (23,'2026_08_15_000004_add_custom_properties_to_media_library_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (24,'2026_09_16_000001_add_cover_media_id_to_posts_and_categories',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (25,'2026_09_16_000002_seed_default_media_folders',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (26,'2026_09_18_210000_add_sticky_level_to_posts_table',3);
@@ -0,0 +1,584 @@
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
DROP TABLE IF EXISTS `cache`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `cache` (
`key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`value` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
`expiration` int NOT NULL,
PRIMARY KEY (`key`),
KEY `cache_expiration_index` (`expiration`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `cache_locks`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `cache_locks` (
`key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`owner` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`expiration` int NOT NULL,
PRIMARY KEY (`key`),
KEY `cache_locks_expiration_index` (`expiration`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `categories`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `categories` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`slug` varchar(120) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`display_order` tinyint NOT NULL DEFAULT '0',
`description` text COLLATE utf8mb4_unicode_ci,
`cover_media_id` bigint unsigned DEFAULT NULL,
`post_count` int unsigned NOT NULL DEFAULT '0',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `categories_slug_unique` (`slug`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `comments`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `comments` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`post_id` bigint unsigned NOT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`author_name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`author_email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`author_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`content` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
`ip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
`ai_review` json DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `comments_user_id_foreign` (`user_id`),
KEY `comments_post_id_status_index` (`post_id`,`status`),
KEY `comments_status_index` (`status`),
CONSTRAINT `comments_post_id_foreign` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE,
CONSTRAINT `comments_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `failed_jobs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `failed_jobs` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`uuid` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`connection` text COLLATE utf8mb4_unicode_ci NOT NULL,
`queue` text COLLATE utf8mb4_unicode_ci NOT NULL,
`payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`exception` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`failed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `failed_jobs_uuid_unique` (`uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `hello_world_logs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `hello_world_logs` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`comment_id` bigint unsigned DEFAULT NULL,
`note` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `hello_world_logs_comment_id_foreign` (`comment_id`),
CONSTRAINT `hello_world_logs_comment_id_foreign` FOREIGN KEY (`comment_id`) REFERENCES `comments` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `job_batches`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `job_batches` (
`id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`total_jobs` int NOT NULL,
`pending_jobs` int NOT NULL,
`failed_jobs` int NOT NULL,
`failed_job_ids` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`options` mediumtext COLLATE utf8mb4_unicode_ci,
`cancelled_at` int DEFAULT NULL,
`created_at` int NOT NULL,
`finished_at` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `jobs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `jobs` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`queue` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`attempts` tinyint unsigned NOT NULL,
`reserved_at` int unsigned DEFAULT NULL,
`available_at` int unsigned NOT NULL,
`created_at` int unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `jobs_queue_index` (`queue`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `links`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `links` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`url` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`note` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`display_order` tinyint NOT NULL DEFAULT '0',
`visible` tinyint(1) NOT NULL DEFAULT '1',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `market_packages`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `market_packages` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`item_key` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
`item_type` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
`title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`price` bigint unsigned NOT NULL,
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
`payment_id` bigint unsigned DEFAULT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `market_packages_item_key_item_type_unique` (`item_key`,`item_type`),
KEY `market_packages_payment_id_foreign` (`payment_id`),
KEY `market_packages_user_id_foreign` (`user_id`),
CONSTRAINT `market_packages_payment_id_foreign` FOREIGN KEY (`payment_id`) REFERENCES `payments` (`id`) ON DELETE SET NULL,
CONSTRAINT `market_packages_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `media`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `media` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`model_id` bigint unsigned NOT NULL,
`uuid` char(36) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`collection_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`file_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`mime_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`disk` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`conversions_disk` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`size` bigint unsigned NOT NULL,
`manipulations` json NOT NULL,
`custom_properties` json NOT NULL,
`generated_conversions` json NOT NULL,
`responsive_images` json NOT NULL,
`order_column` int unsigned DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `media_uuid_unique` (`uuid`),
KEY `media_model_type_model_id_index` (`model_type`,`model_id`),
KEY `media_order_column_index` (`order_column`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `media_folders`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `media_folders` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`parent_id` bigint unsigned DEFAULT NULL,
`code` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`sort` int NOT NULL DEFAULT '0',
`is_active` tinyint(1) NOT NULL DEFAULT '1',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `media_folders_code_unique` (`code`),
KEY `media_folders_parent_id_foreign` (`parent_id`),
CONSTRAINT `media_folders_parent_id_foreign` FOREIGN KEY (`parent_id`) REFERENCES `media_folders` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `media_library`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `media_library` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint unsigned DEFAULT NULL,
`disk` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'public',
`path` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`original_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`extension` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`mime_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`size` bigint unsigned NOT NULL DEFAULT '0',
`type` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'image',
`source` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'admin',
`folder` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`alt_text` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`custom_properties` json DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`deleted_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `media_library_path_unique` (`path`),
KEY `media_library_user_id_foreign` (`user_id`),
KEY `media_library_type_index` (`type`),
KEY `media_library_folder_index` (`folder`),
KEY `media_library_source_index` (`source`),
CONSTRAINT `media_library_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `membership_plans`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `membership_plans` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
`slug` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
`description` text COLLATE utf8mb4_unicode_ci,
`price` bigint unsigned NOT NULL,
`duration_days` int unsigned NOT NULL DEFAULT '30',
`permissions` json DEFAULT NULL,
`active` tinyint(1) NOT NULL DEFAULT '1',
`sort` int unsigned NOT NULL DEFAULT '0',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `membership_plans_slug_unique` (`slug`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `migrations`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `migrations` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`migration` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`batch` int NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `model_has_permissions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `model_has_permissions` (
`permission_id` bigint unsigned NOT NULL,
`model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`model_id` bigint unsigned NOT NULL,
PRIMARY KEY (`permission_id`,`model_id`,`model_type`),
KEY `model_has_permissions_model_id_model_type_index` (`model_id`,`model_type`),
CONSTRAINT `model_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `model_has_roles`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `model_has_roles` (
`role_id` bigint unsigned NOT NULL,
`model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`model_id` bigint unsigned NOT NULL,
PRIMARY KEY (`role_id`,`model_id`,`model_type`),
KEY `model_has_roles_model_id_model_type_index` (`model_id`,`model_type`),
CONSTRAINT `model_has_roles_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `password_reset_tokens`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `password_reset_tokens` (
`email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`token` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `payments`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `payments` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`order_no` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`subject` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`description` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`amount` bigint unsigned NOT NULL,
`channel` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'alipay',
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
`gateway_trade_no` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`payload` json DEFAULT NULL,
`paid_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`payable_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`payable_id` bigint unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `payments_order_no_unique` (`order_no`),
KEY `payments_user_id_status_index` (`user_id`,`status`),
KEY `payments_payable_type_payable_id_index` (`payable_type`,`payable_id`),
CONSTRAINT `payments_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `permissions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `permissions` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`guard_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `permissions_name_guard_name_unique` (`name`,`guard_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `personal_access_tokens`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `personal_access_tokens` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`tokenable_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`tokenable_id` bigint unsigned NOT NULL,
`name` text COLLATE utf8mb4_unicode_ci NOT NULL,
`token` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
`abilities` text COLLATE utf8mb4_unicode_ci,
`last_used_at` timestamp NULL DEFAULT NULL,
`expires_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `personal_access_tokens_token_unique` (`token`),
KEY `personal_access_tokens_tokenable_type_tokenable_id_index` (`tokenable_type`,`tokenable_id`),
KEY `personal_access_tokens_expires_at_index` (`expires_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `plugin_records`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `plugin_records` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`vendor` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
`version` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`enabled` tinyint(1) NOT NULL DEFAULT '1',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `plugin_records_vendor_name_unique` (`vendor`,`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `posts`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `posts` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`category_id` bigint unsigned DEFAULT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`slug` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`excerpt` text COLLATE utf8mb4_unicode_ci,
`content` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`content_format` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'markdown',
`keywords` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`cover_media_id` bigint unsigned DEFAULT NULL,
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'published',
`is_sticky` tinyint(1) NOT NULL DEFAULT '0',
`sticky_level` tinyint unsigned NOT NULL DEFAULT '0',
`close_comment` tinyint(1) NOT NULL DEFAULT '0',
`read_password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`views` bigint unsigned NOT NULL DEFAULT '0',
`comment_count` int unsigned NOT NULL DEFAULT '0',
`meta` json DEFAULT NULL,
`published_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `posts_slug_unique` (`slug`),
KEY `posts_category_id_foreign` (`category_id`),
KEY `posts_user_id_foreign` (`user_id`),
KEY `posts_status_published_at_index` (`status`,`published_at`),
KEY `posts_status_index` (`status`),
KEY `posts_published_at_index` (`published_at`),
CONSTRAINT `posts_category_id_foreign` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL,
CONSTRAINT `posts_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `redirects`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `redirects` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`hash` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL,
`target_url` text COLLATE utf8mb4_unicode_ci NOT NULL,
`clicks` bigint unsigned NOT NULL DEFAULT '0',
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active',
`last_clicked_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `redirects_hash_unique` (`hash`),
KEY `redirects_target_url_index` (`target_url`(191))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `role_has_permissions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `role_has_permissions` (
`permission_id` bigint unsigned NOT NULL,
`role_id` bigint unsigned NOT NULL,
PRIMARY KEY (`permission_id`,`role_id`),
KEY `role_has_permissions_role_id_foreign` (`role_id`),
CONSTRAINT `role_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE,
CONSTRAINT `role_has_permissions_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `roles`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `roles` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`guard_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `roles_name_guard_name_unique` (`name`,`guard_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `sessions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `sessions` (
`id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`ip_address` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`user_agent` text COLLATE utf8mb4_unicode_ci,
`payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`last_activity` int NOT NULL,
PRIMARY KEY (`id`),
KEY `sessions_user_id_index` (`user_id`),
KEY `sessions_last_activity_index` (`last_activity`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `settings`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `settings` (
`key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`value` text COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `subscriptions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `subscriptions` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint unsigned NOT NULL,
`membership_plan_id` bigint unsigned DEFAULT NULL,
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active',
`starts_at` timestamp NULL DEFAULT NULL,
`ends_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `subscriptions_membership_plan_id_foreign` (`membership_plan_id`),
KEY `subscriptions_user_id_status_index` (`user_id`,`status`),
CONSTRAINT `subscriptions_membership_plan_id_foreign` FOREIGN KEY (`membership_plan_id`) REFERENCES `membership_plans` (`id`) ON DELETE SET NULL,
CONSTRAINT `subscriptions_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `taggables`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `taggables` (
`tag_id` bigint unsigned NOT NULL,
`taggable_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`taggable_id` bigint unsigned NOT NULL,
UNIQUE KEY `taggables_tag_id_taggable_id_taggable_type_unique` (`tag_id`,`taggable_id`,`taggable_type`),
KEY `taggables_taggable_type_taggable_id_index` (`taggable_type`,`taggable_id`),
CONSTRAINT `taggables_tag_id_foreign` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `tags`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `tags` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` json NOT NULL,
`slug` json NOT NULL,
`type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`order_column` int DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `users` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`email_verified_at` timestamp NULL DEFAULT NULL,
`password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`legacy_md5` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`logincount` int unsigned NOT NULL DEFAULT '0',
`loginip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`regip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`logintime` timestamp NULL DEFAULT NULL,
`lastpost_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `users_email_unique` (`email`),
KEY `users_legacy_md5_index` (`legacy_md5`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (1,'0001_01_01_000000_create_users_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (2,'0001_01_01_000001_create_cache_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (3,'0001_01_01_000002_create_jobs_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (4,'2026_08_11_092446_create_permission_tables',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (5,'2026_08_11_092448_create_media_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (6,'2026_08_11_092449_create_tag_tables',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (7,'2026_08_11_100000_create_membership_tables',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (8,'2026_08_11_100000_create_payments_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (9,'2026_08_11_100001_create_categories_and_posts_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (10,'2026_08_11_100002_create_comments_and_trackbacks_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (11,'2026_08_11_100003_create_links_and_settings_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (12,'2026_08_11_100004_add_blog_columns_to_users_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (13,'2026_08_11_100005_add_content_format_to_posts_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (14,'2026_08_11_113537_create_personal_access_tokens_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (15,'2026_08_12_000000_create_hello_world_logs_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (16,'2026_08_12_100000_add_payable_to_payments_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (17,'2026_08_12_200000_create_market_packages_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (18,'2026_08_12_300000_add_description_to_categories_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (19,'2026_08_12_400000_create_redirects_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (20,'2026_08_15_000001_create_media_library_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (21,'2026_08_15_000002_create_media_folders_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (22,'2026_08_15_000003_add_soft_deletes_to_media_library_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (23,'2026_08_15_000004_add_custom_properties_to_media_library_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (24,'2026_09_16_000001_add_cover_media_id_to_posts_and_categories',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (25,'2026_09_16_000002_seed_default_media_folders',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (26,'2026_09_18_210000_add_sticky_level_to_posts_table',3);
@@ -0,0 +1,584 @@
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
DROP TABLE IF EXISTS `cache`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `cache` (
`key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`value` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
`expiration` int NOT NULL,
PRIMARY KEY (`key`),
KEY `cache_expiration_index` (`expiration`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `cache_locks`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `cache_locks` (
`key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`owner` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`expiration` int NOT NULL,
PRIMARY KEY (`key`),
KEY `cache_locks_expiration_index` (`expiration`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `categories`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `categories` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`slug` varchar(120) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`display_order` tinyint NOT NULL DEFAULT '0',
`description` text COLLATE utf8mb4_unicode_ci,
`cover_media_id` bigint unsigned DEFAULT NULL,
`post_count` int unsigned NOT NULL DEFAULT '0',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `categories_slug_unique` (`slug`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `comments`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `comments` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`post_id` bigint unsigned NOT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`author_name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`author_email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`author_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`content` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
`ip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
`ai_review` json DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `comments_user_id_foreign` (`user_id`),
KEY `comments_post_id_status_index` (`post_id`,`status`),
KEY `comments_status_index` (`status`),
CONSTRAINT `comments_post_id_foreign` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE,
CONSTRAINT `comments_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `failed_jobs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `failed_jobs` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`uuid` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`connection` text COLLATE utf8mb4_unicode_ci NOT NULL,
`queue` text COLLATE utf8mb4_unicode_ci NOT NULL,
`payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`exception` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`failed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `failed_jobs_uuid_unique` (`uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `hello_world_logs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `hello_world_logs` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`comment_id` bigint unsigned DEFAULT NULL,
`note` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `hello_world_logs_comment_id_foreign` (`comment_id`),
CONSTRAINT `hello_world_logs_comment_id_foreign` FOREIGN KEY (`comment_id`) REFERENCES `comments` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `job_batches`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `job_batches` (
`id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`total_jobs` int NOT NULL,
`pending_jobs` int NOT NULL,
`failed_jobs` int NOT NULL,
`failed_job_ids` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`options` mediumtext COLLATE utf8mb4_unicode_ci,
`cancelled_at` int DEFAULT NULL,
`created_at` int NOT NULL,
`finished_at` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `jobs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `jobs` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`queue` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`attempts` tinyint unsigned NOT NULL,
`reserved_at` int unsigned DEFAULT NULL,
`available_at` int unsigned NOT NULL,
`created_at` int unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `jobs_queue_index` (`queue`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `links`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `links` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`url` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`note` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`display_order` tinyint NOT NULL DEFAULT '0',
`visible` tinyint(1) NOT NULL DEFAULT '1',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `market_packages`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `market_packages` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`item_key` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
`item_type` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
`title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`price` bigint unsigned NOT NULL,
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
`payment_id` bigint unsigned DEFAULT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `market_packages_item_key_item_type_unique` (`item_key`,`item_type`),
KEY `market_packages_payment_id_foreign` (`payment_id`),
KEY `market_packages_user_id_foreign` (`user_id`),
CONSTRAINT `market_packages_payment_id_foreign` FOREIGN KEY (`payment_id`) REFERENCES `payments` (`id`) ON DELETE SET NULL,
CONSTRAINT `market_packages_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `media`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `media` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`model_id` bigint unsigned NOT NULL,
`uuid` char(36) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`collection_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`file_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`mime_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`disk` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`conversions_disk` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`size` bigint unsigned NOT NULL,
`manipulations` json NOT NULL,
`custom_properties` json NOT NULL,
`generated_conversions` json NOT NULL,
`responsive_images` json NOT NULL,
`order_column` int unsigned DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `media_uuid_unique` (`uuid`),
KEY `media_model_type_model_id_index` (`model_type`,`model_id`),
KEY `media_order_column_index` (`order_column`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `media_folders`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `media_folders` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`parent_id` bigint unsigned DEFAULT NULL,
`code` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`sort` int NOT NULL DEFAULT '0',
`is_active` tinyint(1) NOT NULL DEFAULT '1',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `media_folders_code_unique` (`code`),
KEY `media_folders_parent_id_foreign` (`parent_id`),
CONSTRAINT `media_folders_parent_id_foreign` FOREIGN KEY (`parent_id`) REFERENCES `media_folders` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `media_library`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `media_library` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint unsigned DEFAULT NULL,
`disk` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'public',
`path` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`original_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`extension` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`mime_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`size` bigint unsigned NOT NULL DEFAULT '0',
`type` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'image',
`source` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'admin',
`folder` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`alt_text` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`custom_properties` json DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`deleted_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `media_library_path_unique` (`path`),
KEY `media_library_user_id_foreign` (`user_id`),
KEY `media_library_type_index` (`type`),
KEY `media_library_folder_index` (`folder`),
KEY `media_library_source_index` (`source`),
CONSTRAINT `media_library_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `membership_plans`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `membership_plans` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
`slug` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
`description` text COLLATE utf8mb4_unicode_ci,
`price` bigint unsigned NOT NULL,
`duration_days` int unsigned NOT NULL DEFAULT '30',
`permissions` json DEFAULT NULL,
`active` tinyint(1) NOT NULL DEFAULT '1',
`sort` int unsigned NOT NULL DEFAULT '0',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `membership_plans_slug_unique` (`slug`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `migrations`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `migrations` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`migration` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`batch` int NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `model_has_permissions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `model_has_permissions` (
`permission_id` bigint unsigned NOT NULL,
`model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`model_id` bigint unsigned NOT NULL,
PRIMARY KEY (`permission_id`,`model_id`,`model_type`),
KEY `model_has_permissions_model_id_model_type_index` (`model_id`,`model_type`),
CONSTRAINT `model_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `model_has_roles`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `model_has_roles` (
`role_id` bigint unsigned NOT NULL,
`model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`model_id` bigint unsigned NOT NULL,
PRIMARY KEY (`role_id`,`model_id`,`model_type`),
KEY `model_has_roles_model_id_model_type_index` (`model_id`,`model_type`),
CONSTRAINT `model_has_roles_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `password_reset_tokens`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `password_reset_tokens` (
`email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`token` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `payments`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `payments` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`order_no` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`subject` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`description` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`amount` bigint unsigned NOT NULL,
`channel` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'alipay',
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
`gateway_trade_no` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`payload` json DEFAULT NULL,
`paid_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`payable_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`payable_id` bigint unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `payments_order_no_unique` (`order_no`),
KEY `payments_user_id_status_index` (`user_id`,`status`),
KEY `payments_payable_type_payable_id_index` (`payable_type`,`payable_id`),
CONSTRAINT `payments_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `permissions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `permissions` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`guard_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `permissions_name_guard_name_unique` (`name`,`guard_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `personal_access_tokens`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `personal_access_tokens` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`tokenable_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`tokenable_id` bigint unsigned NOT NULL,
`name` text COLLATE utf8mb4_unicode_ci NOT NULL,
`token` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
`abilities` text COLLATE utf8mb4_unicode_ci,
`last_used_at` timestamp NULL DEFAULT NULL,
`expires_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `personal_access_tokens_token_unique` (`token`),
KEY `personal_access_tokens_tokenable_type_tokenable_id_index` (`tokenable_type`,`tokenable_id`),
KEY `personal_access_tokens_expires_at_index` (`expires_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `plugin_records`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `plugin_records` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`vendor` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
`version` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`enabled` tinyint(1) NOT NULL DEFAULT '1',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `plugin_records_vendor_name_unique` (`vendor`,`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `posts`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `posts` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`category_id` bigint unsigned DEFAULT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`slug` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`excerpt` text COLLATE utf8mb4_unicode_ci,
`content` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`content_format` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'markdown',
`keywords` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`cover_media_id` bigint unsigned DEFAULT NULL,
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'published',
`is_sticky` tinyint(1) NOT NULL DEFAULT '0',
`sticky_level` tinyint unsigned NOT NULL DEFAULT '0',
`close_comment` tinyint(1) NOT NULL DEFAULT '0',
`read_password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`views` bigint unsigned NOT NULL DEFAULT '0',
`comment_count` int unsigned NOT NULL DEFAULT '0',
`meta` json DEFAULT NULL,
`published_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `posts_slug_unique` (`slug`),
KEY `posts_category_id_foreign` (`category_id`),
KEY `posts_user_id_foreign` (`user_id`),
KEY `posts_status_published_at_index` (`status`,`published_at`),
KEY `posts_status_index` (`status`),
KEY `posts_published_at_index` (`published_at`),
CONSTRAINT `posts_category_id_foreign` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL,
CONSTRAINT `posts_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `redirects`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `redirects` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`hash` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL,
`target_url` text COLLATE utf8mb4_unicode_ci NOT NULL,
`clicks` bigint unsigned NOT NULL DEFAULT '0',
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active',
`last_clicked_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `redirects_hash_unique` (`hash`),
KEY `redirects_target_url_index` (`target_url`(191))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `role_has_permissions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `role_has_permissions` (
`permission_id` bigint unsigned NOT NULL,
`role_id` bigint unsigned NOT NULL,
PRIMARY KEY (`permission_id`,`role_id`),
KEY `role_has_permissions_role_id_foreign` (`role_id`),
CONSTRAINT `role_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE,
CONSTRAINT `role_has_permissions_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `roles`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `roles` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`guard_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `roles_name_guard_name_unique` (`name`,`guard_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `sessions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `sessions` (
`id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`user_id` bigint unsigned DEFAULT NULL,
`ip_address` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`user_agent` text COLLATE utf8mb4_unicode_ci,
`payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`last_activity` int NOT NULL,
PRIMARY KEY (`id`),
KEY `sessions_user_id_index` (`user_id`),
KEY `sessions_last_activity_index` (`last_activity`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `settings`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `settings` (
`key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`value` text COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `subscriptions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `subscriptions` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint unsigned NOT NULL,
`membership_plan_id` bigint unsigned DEFAULT NULL,
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active',
`starts_at` timestamp NULL DEFAULT NULL,
`ends_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `subscriptions_membership_plan_id_foreign` (`membership_plan_id`),
KEY `subscriptions_user_id_status_index` (`user_id`,`status`),
CONSTRAINT `subscriptions_membership_plan_id_foreign` FOREIGN KEY (`membership_plan_id`) REFERENCES `membership_plans` (`id`) ON DELETE SET NULL,
CONSTRAINT `subscriptions_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `taggables`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `taggables` (
`tag_id` bigint unsigned NOT NULL,
`taggable_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`taggable_id` bigint unsigned NOT NULL,
UNIQUE KEY `taggables_tag_id_taggable_id_taggable_type_unique` (`tag_id`,`taggable_id`,`taggable_type`),
KEY `taggables_taggable_type_taggable_id_index` (`taggable_type`,`taggable_id`),
CONSTRAINT `taggables_tag_id_foreign` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `tags`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `tags` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` json NOT NULL,
`slug` json NOT NULL,
`type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`order_column` int DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `users` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`email_verified_at` timestamp NULL DEFAULT NULL,
`password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`legacy_md5` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`logincount` int unsigned NOT NULL DEFAULT '0',
`loginip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`regip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`logintime` timestamp NULL DEFAULT NULL,
`lastpost_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `users_email_unique` (`email`),
KEY `users_legacy_md5_index` (`legacy_md5`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (1,'0001_01_01_000000_create_users_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (2,'0001_01_01_000001_create_cache_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (3,'0001_01_01_000002_create_jobs_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (4,'2026_08_11_092446_create_permission_tables',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (5,'2026_08_11_092448_create_media_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (6,'2026_08_11_092449_create_tag_tables',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (7,'2026_08_11_100000_create_membership_tables',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (8,'2026_08_11_100000_create_payments_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (9,'2026_08_11_100001_create_categories_and_posts_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (10,'2026_08_11_100002_create_comments_and_trackbacks_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (11,'2026_08_11_100003_create_links_and_settings_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (12,'2026_08_11_100004_add_blog_columns_to_users_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (13,'2026_08_11_100005_add_content_format_to_posts_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (14,'2026_08_11_113537_create_personal_access_tokens_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (15,'2026_08_12_000000_create_hello_world_logs_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (16,'2026_08_12_100000_add_payable_to_payments_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (17,'2026_08_12_200000_create_market_packages_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (18,'2026_08_12_300000_add_description_to_categories_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (19,'2026_08_12_400000_create_redirects_table',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (20,'2026_08_15_000001_create_media_library_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (21,'2026_08_15_000002_create_media_folders_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (22,'2026_08_15_000003_add_soft_deletes_to_media_library_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (23,'2026_08_15_000004_add_custom_properties_to_media_library_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (24,'2026_09_16_000001_add_cover_media_id_to_posts_and_categories',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (25,'2026_09_16_000002_seed_default_media_folders',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (26,'2026_09_18_210000_add_sticky_level_to_posts_table',3);
+21 -11
View File
@@ -35,17 +35,17 @@ routes/web.php # 前台规范路由;routes/legacy.php 老 URL 301(新增
### 钩子清单
| Hook | 类型 | 参数 | 用途 |
|------|------|------|------|
| `post.rendered` | filter | `(string $html, Post): string` | 文章渲染后处理 |
| `comment.created` | action | `Comment` | 新评论创建 |
| `payment.paid` | action | `Payment` | 支付成功分发 |
| `filament.post_form` | action | `Schema` | 文章表单注入 |
| `filament.post_table` | action | `Table` | 文章表格注入 |
| `seo.structured_data` | filter | `(array): array` | JSON-LD 扩展 |
| `theme.inject.{point}` | filter | `(string): string` | 注入点追加 HTML |
| Hook | 类型 | 参数 | 用途 |
| ---------------------- | ------ | ------------------------------ | --------------- |
| `post.rendered` | filter | `(string $html, Post): string` | 文章渲染后处理 |
| `comment.created` | action | `Comment` | 新评论创建 |
| `payment.paid` | action | `Payment` | 支付成功分发 |
| `filament.post_form` | action | `Schema` | 文章表单注入 |
| `filament.post_table` | action | `Table` | 文章表格注入 |
| `seo.structured_data` | filter | `(array): array` | JSON-LD 扩展 |
| `theme.inject.{point}` | filter | `(string): string` | 注入点追加 HTML |
> `Schema::components()` / `Table::columns()` 是**替换**语义:表单用 `[...$schema->getComponents(), Section::make(...)]` 合并,表格用 `pushColumns()`。
> 文章表单是左 2 右 1:插件用 `PostForm::appendToSidebar()` / `PostForm::appendToAccessSection()` 注入,不要往根节点追加整行 Section。表格仍用 `pushColumns()`。
## 4. 主题规范(详见 docs/themes.md)
@@ -74,7 +74,17 @@ routes/web.php # 前台规范路由;routes/legacy.php 老 URL 301(新增
- 不提交:`.env`、密钥/证书、`storage/media-library/temp/`、构建产物
- 完成后更新对应 `docs/` 文档
## 8. 常用命令速查
## 8. 后台导航约定
- 顶栏 **网站** 打开前台新窗口,**写文章** 进新建文章
- **管理 → 页面缓存**:查看/清理前台整页缓存,不要只藏在用户菜单里
- **管理 → 语言**:只展示当前 `APP_LOCALE` 和已有语言包。文章还没有多语言版本,不要假装已经做了
- 文章置顶用 `sticky_level`(0/1/2/3),列表排序 `orderedForList()`。3 = 置顶头条,永远最前
- 前台 RSS / Sitemap / llms.txt 开新窗口。`/llms.txt` 和 `/posts/{slug}.md` 是核心路由,不要做成静态文件,新文章要马上出现
- 首页浏览模式:`home_view_mode` + `?view=list|full` + cookie `ll_home_view`。分页用 `blog_paginate()`(`onEachSide(1)`)
- 老 sablog 正文清洗放 `SablogHtmlNormalizer`,导入 / 渲染 / `sablog:fix-content` 共用,不要各写一套 strip_tags
## 9. 常用命令速查
```bash
php artisan test # 全量测试
+7
View File
@@ -0,0 +1,7 @@
# Diary
## 2026-09-18
- Edit post crashed with `Class "Filament\Tables\Actions\Action" not found`.
- Cause: `CommentsRelationManager` still imported the Filament v3 class. HTTP GET on the edit page can pass because the relation table only instantiates when its Livewire component boots.
- Fix: `use Filament\Actions\Action`. Regression: `FilamentV5CompatTest`.
+8 -1
View File
@@ -65,7 +65,7 @@ php artisan sablog:import \
- 伪静态:`/show-12-1.html` → 新文章页;`/category-3-1.html` → 新分类页;`/archives-202008-1.html` → 新归档
- 查询串:`/?action=show&id=12`、`/?action=search&keyword=x`、`/?action=tags&id=1` …全部映射
- 稳定路径:`/rss.xml`、`/sitemap.xml`、`/robots.txt` 保持原样
- 稳定路径:`/rss.xml`、`/sitemap.xml`、`/robots.txt`、`/llms.txt` 保持原样
- 老入口:`attachment.php?id=` → 301 到 S3/本地媒体地址;`tburl.php`/`trackback.php` → 410
## 附件上 S3
@@ -86,8 +86,15 @@ php artisan attachments:sync-s3 --legacy-dir=... --force
php artisan content:convert --all # 全部 HTML 文章转 Markdown
php artisan content:convert 12 # 单篇
php artisan content:convert --all --dry-run # 先预览
# 清洗已导入正文:字面 &nbsp;、SyntaxHighlighter、docker 分行 div
php artisan sablog:fix-content --all
php artisan sablog:fix-content 3156
php artisan sablog:fix-content --all --dry-run
```
渲染时也会做同样的清洗,所以不跑修复命令,前台也能先看起来正常。`content:convert` 和 `--convert-markdown` 会先清洗再转 Markdown。
## 幂等与重跑
- 导入按**原始 ID** `updateOrInsert`:重复执行不会产生重复数据,只会覆盖更新
+8 -9
View File
@@ -103,21 +103,20 @@ class ServiceProvider extends PluginServiceProvider
核心资源的表单/表格会触发 `filament.post_form` / `filament.post_table` 两个钩子,插件可追加自己的字段与列,停用插件后字段自动消失,核心零侵入。
> 注意:`Schema::components()` 与 `Table::columns()` 都是**整体替换**语义,追加要这样写:
> 文章编辑页是 **左正文 2 : 右功能 1**。插件不要往根节点再追加整行 Section,否则会把 2:1 挤掉。
>
> - 表单:`$schema->components([...$schema->getComponents(), Section::make(...)])`
> - 表单:用 `PostForm::appendToSidebar()` / `PostForm::appendToAccessSection()`
> - 表格:`$table->pushColumns([...])`
示例(给文章加「付费设置」):
示例(给右侧「访问设置」加会员字段):
```php
use App\Filament\Resources\Posts\Schemas\PostForm;
$manager->addAction('filament.post_form', function (Schema $schema) {
$schema->components([
...$schema->getComponents(),
Section::make('付费设置')->columns(3)->collapsible()->schema([
TextInput::make('meta.price')->label('单篇价格(分)')->numeric()->default(0),
Toggle::make('meta.members_only')->label('会员专享'),
]),
PostForm::appendToAccessSection($schema, [
TextInput::make('meta.price')->label('价格')->numeric()->default(0),
Toggle::make('meta.members_only')->label('会员专享'),
]);
});
+8 -8
View File
@@ -76,14 +76,14 @@ class ServiceProvider extends PluginServiceProvider
### 常用钩子
| Hook | 类型 | 参数 | 用途 |
| --------------------- | ------ | ------------------------------- | --------------------------------------- |
| `post.rendered` | filter | `(string $html, Post) : string` | 文章渲染后处理(短代码/付费过滤) |
| `comment.created` | action | `Comment` | 新评论创建 |
| `payment.paid` | action | `Payment` | 支付成功(按 `$payment->payable` 分发) |
| `filament.post_form` | action | `Schema` | 文章编辑表单追加字段 |
| `filament.post_table` | action | `Table` | 文章列表追加列 |
| `seo.structured_data` | filter | `(array $data) : array` | 扩展 JSON-LD |
| Hook | 类型 | 参数 | 用途 |
| --------------------- | ------ | ------------------------------- | ------------------------------------------------ |
| `post.rendered` | filter | `(string $html, Post) : string` | 文章渲染后处理(短代码/付费过滤) |
| `comment.created` | action | `Comment` | 新评论创建 |
| `payment.paid` | action | `Payment` | 支付成功(按 `$payment->payable` 分发) |
| `filament.post_form` | action | `Schema` | 文章表单注入(用 `PostForm::appendToSidebar()`) |
| `filament.post_table` | action | `Table` | 文章列表追加列 |
| `seo.structured_data` | filter | `(array $data) : array` | 扩展 JSON-LD |
完整列表见 `docs/plugins.md`。
@@ -6,6 +6,7 @@ namespace Plugins\Demo\HelloWorld;
use App\Blog\Support\PluginManager;
use App\Blog\Support\PluginServiceProvider;
use App\Filament\Resources\Posts\Schemas\PostForm;
use App\Models\Comment;
use App\Models\Post;
use App\Models\Setting;
@@ -53,8 +54,7 @@ class ServiceProvider extends PluginServiceProvider
// 5. 后台表单注入示例:给文章编辑页加一个演示字段(meta.demo_note)
$manager->addAction('filament.post_form', function (Schema $schema) {
$schema->components([
...$schema->getComponents(),
PostForm::appendToSidebar($schema, [
Section::make('Hello World 演示')
->collapsible()
->schema([
+3 -3
View File
@@ -1,9 +1,9 @@
{
"title": "SEO & GEO",
"version": "1.0.0",
"description": "完善 SEO(结构化数据/OG 标签)+ GEO(llms.txt / Markdown 导出,面向 AI 搜索引擎优化)",
"version": "1.0.1",
"description": "完善 SEO(结构化数据/OG 标签)。llms.txt 和 Markdown 导出由核心路由提供。",
"author": "LaraLog",
"type": "core",
"provider": "Plugins\\Neatstudio\\BlogSeo\\ServiceProvider",
"usage": "无需额外配置,启用即生效:\n- SEO:页面 title/description/keywords、OG/Twitter 标签、JSON-LD(BlogPosting/WebSite)自动输出\n- GEO:/llms.txt 站点摘要、/posts/{slug}.md 文章 Markdown 导出\n- /sitemap.xml、/robots.txt 由核心路由提供\n站点名/描述等在「博客设置 → 站点」配置,社交账号在「博客设置 → 社交账号」配置"
"usage": "无需额外配置,启用即生效:\n- SEO:页面 title/description/keywords、OG/Twitter 标签、JSON-LD(BlogPosting/WebSite)自动输出\n- GEO:核心路由 /llms.txt、/posts/{slug}.md(停用本插件后仍可用)\n- /sitemap.xml、/robots.txt 由核心路由提供\n站点名/描述等在「博客设置 → 站点」配置,社交账号在「博客设置 → 社交账号」配置"
}
@@ -1,11 +0,0 @@
<?php
declare(strict_types=1);
use Illuminate\Support\Facades\Route;
use Plugins\Neatstudio\BlogSeo\Http\SeoController;
// GEO:面向 AI 搜索引擎(LLM 爬虫)
Route::get('/llms.txt', [SeoController::class, 'llmsTxt'])->name('geo.llms');
Route::get('/posts/{slug}.md', [SeoController::class, 'postMarkdown'])->where('slug', '[A-Za-z0-9\-]+')->name('geo.post.markdown');
@@ -1,71 +0,0 @@
<?php
declare(strict_types=1);
namespace Plugins\Neatstudio\BlogSeo\Http;
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class SeoController
{
/**
* GEO:/llms.txt — 面向 LLM 的站点索引
*/
public function llmsTxt()
{
$siteName = blog_setting('site_name', config('blog.name'));
$siteDescription = blog_setting('site_description', config('blog.description'));
$posts = Post::published()->latest('published_at')->limit(200)->get();
$lines = [];
$lines[] = '# '.$siteName;
$lines[] = '';
$lines[] = '> '.$siteDescription;
$lines[] = '';
$lines[] = '## 文章(Markdown 全文可访问 /posts/{slug}.md)';
$lines[] = '';
foreach ($posts as $post) {
$lines[] = '- ['.$post->title.']('.route('posts.show', $post->id).') — '.Str::limit(strip_tags($post->excerpt_or_fallback), 120);
}
return response(implode("\n", $lines)."\n")
->header('Content-Type', 'text/plain; charset=utf-8');
}
/**
* GEO:/posts/{slug}.md — 面向 AI 爬虫的纯 Markdown 导出
*/
public function postMarkdown(Request $request, string $slug)
{
$post = Post::query()->where('slug', $slug)->firstOrFail();
if ($post->status !== 'published') {
abort(404);
}
$body = $post->content_format === 'markdown' ? $post->content : $this->htmlToMarkdown($post->content);
$out = "# {$post->title}\n\n";
if ($post->excerpt) {
$out .= '> '.$post->excerpt."\n\n";
}
$out .= $body."\n";
return response($out)->header('Content-Type', 'text/markdown; charset=utf-8');
}
private function htmlToMarkdown(string $html): string
{
$converter = new \League\HTMLToMarkdown\HtmlConverter([
'strip_tags' => false,
'hard_break' => true,
]);
return $converter->convert($html);
}
}
@@ -2,20 +2,16 @@
declare(strict_types=1);
namespace Plugins\Neatstudio\BlogSeo;
use App\Blog\Support\PluginManager;
use App\Blog\Support\PluginServiceProvider;
use Plugins\Neatstudio\BlogSeo\Http\Middleware\SeoMiddleware;
use Plugins\Neatstudio\BlogSeo\Http\SeoController;
class ServiceProvider extends PluginServiceProvider
{
protected function boot(PluginManager $manager): void
{
$this->loadRoutes(__DIR__.'/../routes/web.php');
app('router')->pushMiddlewareToGroup('web', SeoMiddleware::class);
$manager->addFilter('seo.structured_data', function (array $data) {
@@ -2,15 +2,14 @@
declare(strict_types=1);
namespace Plugins\Neatstudio\Membership;
use App\Blog\Support\PluginManager;
use App\Blog\Support\PluginServiceProvider;
use App\Filament\Resources\Posts\Schemas\PostForm;
use App\Models\Post;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
@@ -53,7 +52,7 @@ class ServiceProvider extends PluginServiceProvider
}, 10);
// 付费文章:[paid]...[/paid] —— 无权限访客替换为 teaser;有权限替换为付费内容块(避免原始标签裸露在页面)
$manager->addFilter('post.rendered', function (string $html, \App\Models\Post $post) {
$manager->addFilter('post.rendered', function (string $html, Post $post) {
$service = app(Services\MembershipService::class);
$user = auth()->user();
@@ -81,25 +80,18 @@ class ServiceProvider extends PluginServiceProvider
return $html;
}, 10);
// 后台文章表单注入:付费设置(meta.price 单篇价格 + meta.members_only 会员专享)
// 后台文章表单注入:右侧「访问设置」追加会员专享 / 价格
$manager->addAction('filament.post_form', function (Schema $schema) {
$schema->components([
...$schema->getComponents(),
Section::make('付费设置')
->description('需启用支付插件并配置支付渠道;0 = 免费')
->columns(3)
->collapsible()
->schema([
TextInput::make('meta.price')
->label('单篇价格(分)')
->numeric()
->minValue(0)
->default(0)
->helperText('例:9900 = ¥99;填 0 表示单篇免费'),
Toggle::make('meta.members_only')
->label('会员专享')
->helperText('整篇文章仅会员可读'),
]),
PostForm::appendToAccessSection($schema, [
Toggle::make('meta.members_only')
->label('会员专享')
->helperText('整篇文章仅会员可读'),
TextInput::make('meta.price')
->label('价格')
->numeric()
->minValue(0)
->default(0)
->helperText('单位:分,例 9900 = ¥99;0 表示单篇免费'),
]);
});
+509 -105
View File
@@ -1,40 +1,112 @@
/* ============ LaraLog 默认主题 ============ */
* { box-sizing: border-box; margin: 0; padding: 0; }
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
"Hiragino Sans GB", "Microsoft YaHei", sans-serif;
font-size: 16px;
line-height: 1.7;
color: #333;
background: #f5f6f8;
}
a { color: #2d6cdf; text-decoration: none; }
a:hover { color: #1a4fa8; }
a {
color: #2d6cdf;
text-decoration: none;
}
a:hover {
color: #1a4fa8;
}
.container { max-width: 1100px; margin: 0 auto; padding: 0 16px; }
.container {
max-width: 1100px;
margin: 0 auto;
padding: 0 16px;
}
/* Header */
.site-header { background: #fff; border-bottom: 1px solid #e5e7eb; padding: 18px 0; }
.site-header .container { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; }
.site-title { font-size: 26px; font-weight: 800; color: #111; }
.site-desc { font-size: 13px; color: #888; margin-top: 2px; }
.site-nav { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; font-size: 15px; }
.site-nav a.active { color: #2d6cdf; font-weight: 600; }
.site-search input { padding: 6px 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; width: 150px; }
.inline-form { display: inline; }
.link-btn { background: none; border: none; color: #2d6cdf; cursor: pointer; font-size: 15px; padding: 0; }
.site-header {
background: #fff;
border-bottom: 1px solid #e5e7eb;
padding: 18px 0;
}
.site-header .container {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
}
.site-title {
font-size: 26px;
font-weight: 800;
color: #111;
}
.site-desc {
font-size: 13px;
color: #888;
margin-top: 2px;
}
.site-nav {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
font-size: 15px;
}
.site-nav a.active {
color: #2d6cdf;
font-weight: 600;
}
.site-search input {
padding: 6px 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
width: 150px;
}
.inline-form {
display: inline;
}
.link-btn {
background: none;
border: none;
color: #2d6cdf;
cursor: pointer;
font-size: 15px;
padding: 0;
}
/* Layout */
.main-layout { display: grid; grid-template-columns: 1fr 320px; gap: 24px; padding: 24px 16px; align-items: start; }
.main-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 320px;
gap: 24px;
padding: 24px 16px;
align-items: start;
}
@media (max-width: 860px) {
.main-layout { grid-template-columns: 1fr; }
.main-layout {
grid-template-columns: 1fr;
}
}
/* Content */
.content { min-width: 0; }
.content {
min-width: 0;
}
.post-card, .post-full, .widget, .auth-box, .profile-box, .links-list {
.post-card,
.post-full,
.widget,
.auth-box,
.profile-box,
.links-list {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 10px;
@@ -42,114 +114,446 @@ a:hover { color: #1a4fa8; }
margin-bottom: 20px;
}
.post-card.sticky { border-left: 4px solid #f59e0b; }
.post-card.sticky {
border-left: 4px solid #f59e0b;
}
.post-title { font-size: 20px; line-height: 1.4; margin-bottom: 8px; }
.post-card .post-title a { color: #111; }
.post-card .post-title a:hover { color: #2d6cdf; }
.badge { background: #f59e0b; color: #fff; font-size: 12px; padding: 2px 6px; border-radius: 4px; margin-right: 6px; vertical-align: middle; }
.post-title {
font-size: 20px;
line-height: 1.4;
margin-bottom: 8px;
}
.post-card .post-title a {
color: #111;
}
.post-card .post-title a:hover {
color: #2d6cdf;
}
.badge {
background: #f59e0b;
color: #fff;
font-size: 12px;
padding: 2px 6px;
border-radius: 4px;
margin-right: 6px;
vertical-align: middle;
}
.post-meta { font-size: 13px; color: #888; margin-bottom: 10px; }
.post-excerpt { color: #555; margin-bottom: 10px; }
.post-footer { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: 14px; }
.read-more { font-weight: 600; }
.post-meta {
font-size: 13px;
color: #888;
margin-bottom: 10px;
}
.post-excerpt {
color: #555;
margin-bottom: 10px;
}
.post-footer {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
font-size: 14px;
}
.read-more {
font-weight: 600;
}
.post-full .post-title { font-size: 28px; }
.post-cover { margin: -20px -24px 16px; border-radius: 10px 10px 0 0; overflow: hidden; }
.post-cover img { width: 100%; height: auto; display: block; max-height: 420px; object-fit: cover; }
.post-card .post-cover { margin: -20px -24px 12px; border-radius: 10px 10px 0 0; }
.post-card .post-cover img { max-height: 260px; }
.post-content { margin-top: 16px; font-size: 16.5px; }
.post-content img { max-width: 100%; height: auto; border-radius: 6px; }
.post-content pre { background: #1e1e2e; color: #e6e6e6; padding: 14px; border-radius: 8px; overflow-x: auto; margin: 12px 0; }
.post-content code { background: #f0f1f3; padding: 2px 5px; border-radius: 4px; font-size: 14px; }
.post-content pre code { background: none; padding: 0; }
.post-content blockquote { border-left: 4px solid #2d6cdf; padding-left: 14px; color: #666; margin: 12px 0; }
.post-content h1, .post-content h2, .post-content h3 { margin: 18px 0 8px; line-height: 1.3; }
.post-content ul, .post-content ol { padding-left: 24px; margin: 8px 0; }
.post-content table { border-collapse: collapse; width: 100%; margin: 12px 0; }
.post-content th, .post-content td { border: 1px solid #ddd; padding: 6px 10px; }
.post-full .post-title {
font-size: 28px;
}
.post-cover {
margin: -20px -24px 16px;
border-radius: 10px 10px 0 0;
overflow: hidden;
}
.post-cover img {
width: 100%;
height: auto;
display: block;
max-height: 420px;
object-fit: cover;
}
.post-card .post-cover {
margin: -20px -24px 12px;
border-radius: 10px 10px 0 0;
}
.post-card .post-cover img {
max-height: 260px;
}
.post-content {
margin-top: 16px;
font-size: 16.5px;
max-width: 100%;
overflow-wrap: anywhere;
word-break: break-word;
}
.post-content img {
max-width: 100%;
height: auto;
border-radius: 6px;
}
.post-content pre,
.post-content .codeText,
.post-content ol[class^="dp-"] {
background: #1e1e2e;
color: #e6e6e6;
padding: 14px;
border-radius: 8px;
overflow-x: auto;
max-width: 100%;
margin: 12px 0;
white-space: pre-wrap;
word-break: break-word;
}
.post-content code {
background: #f0f1f3;
padding: 2px 5px;
border-radius: 4px;
font-size: 14px;
word-break: break-all;
overflow-wrap: anywhere;
white-space: break-spaces;
}
.post-content pre code {
display: block;
background: none;
padding: 0;
white-space: pre-wrap;
word-break: break-word;
}
.post-content blockquote {
border-left: 4px solid #2d6cdf;
padding-left: 14px;
color: #666;
margin: 12px 0;
}
.post-content h1,
.post-content h2,
.post-content h3 {
margin: 18px 0 8px;
line-height: 1.3;
}
.post-content ul,
.post-content ol {
padding-left: 24px;
margin: 8px 0;
}
.post-content table {
border-collapse: collapse;
width: 100%;
margin: 12px 0;
}
.post-content th,
.post-content td {
border: 1px solid #ddd;
padding: 6px 10px;
}
.post-tags { margin-top: 16px; }
.tag-link { display: inline-block; background: #eef2ff; color: #3b5bdb; border-radius: 4px; padding: 2px 8px; font-size: 13px; margin: 2px 4px 2px 0; }
.tag-link:hover { background: #dbe4ff; }
.post-tags {
margin-top: 16px;
}
.tag-link {
display: inline-block;
background: #eef2ff;
color: #3b5bdb;
border-radius: 4px;
padding: 2px 8px;
font-size: 13px;
margin: 2px 4px 2px 0;
}
.tag-link:hover {
background: #dbe4ff;
}
/* Comments */
.comments-section { background: #fff; border: 1px solid #e5e7eb; border-radius: 10px; padding: 20px 24px; margin-bottom: 20px; }
.section-title { font-size: 18px; margin-bottom: 14px; }
.comment { padding: 12px 0; border-bottom: 1px solid #f0f1f3; }
.comment:last-of-type { border-bottom: none; }
.comment-head { font-size: 14px; margin-bottom: 4px; }
.comment-time { color: #999; }
.comment-body { font-size: 15px; color: #444; }
.comments-section {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 10px;
padding: 20px 24px;
margin-bottom: 20px;
}
.section-title {
font-size: 18px;
margin-bottom: 14px;
}
.comment {
padding: 12px 0;
border-bottom: 1px solid #f0f1f3;
}
.comment:last-of-type {
border-bottom: none;
}
.comment-head {
font-size: 14px;
margin-bottom: 4px;
}
.comment-time {
color: #999;
}
.comment-body {
font-size: 15px;
color: #444;
}
.comment-form { margin-top: 16px; }
.form-row { margin-bottom: 12px; }
.form-row label { display: block; font-size: 14px; margin-bottom: 4px; }
.form-row input, .form-row textarea {
width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 15px; font-family: inherit;
.comment-form {
margin-top: 16px;
}
.form-row {
margin-bottom: 12px;
}
.form-row label {
display: block;
font-size: 14px;
margin-bottom: 4px;
}
.form-row input,
.form-row textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 15px;
font-family: inherit;
}
.honeypot {
position: absolute;
left: -9999px;
opacity: 0;
}
.error {
color: #e03131;
font-size: 13px;
margin-top: 4px;
}
.honeypot { position: absolute; left: -9999px; opacity: 0; }
.error { color: #e03131; font-size: 13px; margin-top: 4px; }
.btn {
background: #2d6cdf; color: #fff; border: none; padding: 9px 20px; border-radius: 6px;
font-size: 15px; cursor: pointer;
background: #2d6cdf;
color: #fff;
border: none;
padding: 9px 20px;
border-radius: 6px;
font-size: 15px;
cursor: pointer;
}
.btn:hover {
background: #1a4fa8;
}
.btn-primary {
background: #2d6cdf;
}
.btn:hover { background: #1a4fa8; }
.btn-primary { background: #2d6cdf; }
.alert { padding: 10px 16px; border-radius: 8px; margin-bottom: 14px; font-size: 14px; }
.alert-success { background: #d3f9d8; color: #2b8a3e; }
.alert-error { background: #ffe3e3; color: #c92a2a; }
.alert {
padding: 10px 16px;
border-radius: 8px;
margin-bottom: 14px;
font-size: 14px;
}
.alert-success {
background: #d3f9d8;
color: #2b8a3e;
}
.alert-error {
background: #ffe3e3;
color: #c92a2a;
}
.alert-warning {
background: #fff3bf;
color: #8a6d00;
}
/* Sidebar */
.sidebar { position: sticky; top: 20px; }
.widget { padding: 16px 20px; }
.widget-title { font-size: 15px; margin-bottom: 10px; padding-bottom: 8px; border-bottom: 2px solid #2d6cdf; }
.widget-list { list-style: none; }
.widget-list li { font-size: 14px; padding: 4px 0; }
.count { color: #aaa; font-size: 13px; }
.tag-cloud { line-height: 2; }
.tag-cloud.big { line-height: 2.4; }
.sidebar {
position: sticky;
top: 20px;
}
.widget {
padding: 16px 20px;
}
.widget-title {
font-size: 15px;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 2px solid #2d6cdf;
}
.widget-list {
list-style: none;
}
.widget-list li {
font-size: 14px;
padding: 4px 0;
}
.count {
color: #aaa;
font-size: 13px;
}
.tag-cloud {
line-height: 2;
}
.tag-cloud.big {
line-height: 2.4;
}
/* Misc */
.empty { color: #999; text-align: center; padding: 30px 0; }
.list-title { font-size: 22px; margin-bottom: 16px; }
.archive-year { font-size: 17px; margin: 14px 0 6px; }
.archive-list { list-style: none; }
.archive-posts { margin: 4px 0 10px 22px; }
.archive-posts time { color: #aaa; font-size: 13px; }
.pagination { margin: 16px 0; }
.pagination-links { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.pagination-links a, .pagination-links .current, .pagination-links .disabled {
padding: 6px 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; background: #fff;
.empty {
color: #999;
text-align: center;
padding: 30px 0;
}
.list-title {
font-size: 22px;
margin-bottom: 16px;
}
.archive-year {
font-size: 17px;
margin: 14px 0 6px;
}
.archive-list {
list-style: none;
}
.archive-posts {
margin: 4px 0 10px 22px;
}
.archive-posts time {
color: #aaa;
font-size: 13px;
}
.pagination-links .current { background: #2d6cdf; color: #fff; border-color: #2d6cdf; }
.pagination-links .disabled { color: #bbb; }
.auth-layout { display: flex; justify-content: center; padding: 40px 16px; }
.auth-box { width: 100%; max-width: 420px; }
.auth-alt { margin-top: 14px; font-size: 14px; color: #666; text-align: center; }
.view-mode-switcher {
font-size: 13px;
color: #888;
margin-bottom: 14px;
}
.view-mode-switcher a.active {
font-weight: 700;
color: #2d6cdf;
}
.links-list { list-style: none; }
.links-list li { padding: 6px 0; }
.link-note { color: #999; font-size: 14px; }
.pagination {
width: 100%;
max-width: 100%;
min-width: 0;
margin: 16px 0;
}
.pagination-links {
display: flex;
gap: 6px;
flex-wrap: wrap;
align-items: center;
max-width: 100%;
}
.pagination-info {
color: #888;
font-size: 13px;
margin-right: 6px;
white-space: nowrap;
}
.pagination-links a,
.pagination-links .current,
.pagination-links .disabled {
padding: 6px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
background: #fff;
}
.pagination-links .current {
background: #2d6cdf;
color: #fff;
border-color: #2d6cdf;
}
.pagination-links .disabled {
color: #bbb;
}
.site-footer { background: #fff; border-top: 1px solid #e5e7eb; padding: 20px 0; margin-top: 24px; text-align: center; font-size: 13px; color: #888; }
.powered { margin-top: 4px; }
.auth-layout {
display: flex;
justify-content: center;
padding: 40px 16px;
}
.auth-box {
width: 100%;
max-width: 420px;
}
.auth-alt {
margin-top: 14px;
font-size: 14px;
color: #666;
text-align: center;
}
.links-list {
list-style: none;
}
.links-list li {
padding: 6px 0;
}
.link-note {
color: #999;
font-size: 14px;
}
.site-footer {
background: #fff;
border-top: 1px solid #e5e7eb;
padding: 20px 0;
margin-top: 24px;
text-align: center;
font-size: 13px;
color: #888;
}
.powered {
margin-top: 4px;
}
/* TOC */
.toc { background: #f8f9fa; border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px 16px; margin: 0 0 18px; font-size: 14px; }
.toc summary { cursor: pointer; font-weight: 600; margin-bottom: 6px; }
.toc-list { list-style: none; }
.toc-list li { padding: 2px 0; }
.toc-list a { color: #555; }
.toc-list a:hover { color: #2d6cdf; }
.toc-level-2 { padding-left: 0; }
.toc-level-3 { padding-left: 14px; }
.toc-level-4, .toc-level-5, .toc-level-6 { padding-left: 28px; }
.toc {
background: #f8f9fa;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 12px 16px;
margin: 0 0 18px;
font-size: 14px;
}
.toc summary {
cursor: pointer;
font-weight: 600;
margin-bottom: 6px;
}
.toc-list {
list-style: none;
}
.toc-list li {
padding: 2px 0;
}
.toc-list a {
color: #555;
}
.toc-list a:hover {
color: #2d6cdf;
}
.toc-level-2 {
padding-left: 0;
}
.toc-level-3 {
padding-left: 14px;
}
.toc-level-4,
.toc-level-5,
.toc-level-6 {
padding-left: 28px;
}
/* 屏幕阅读器专用(SEO h1 隐藏) */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
+760 -72
View File
@@ -17,88 +17,776 @@
--border: #2c2c2c;
}
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; font-size: 16px; line-height: 1.8; }
a { color: var(--accent); text-decoration: none; }
a:hover { opacity: 0.8; }
.m-container { max-width: 720px; margin: 0 auto; padding: 0 20px; }
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: var(--bg);
color: var(--text);
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
"Microsoft YaHei", sans-serif;
font-size: 16px;
line-height: 1.8;
}
a {
color: var(--accent);
text-decoration: none;
}
a:hover {
opacity: 0.8;
}
/* 页面统一宽度:页头/页脚与主体对齐(默认视图的 .container 也落在这里) */
.container,
.m-container {
max-width: 1080px;
margin: 0 auto;
padding: 0 20px;
}
.m-header { border-bottom: 1px solid var(--border); background: var(--card); }
.m-header .m-container { display: flex; justify-content: space-between; align-items: center; padding: 18px 20px; }
.m-logo { font-size: 22px; font-weight: 800; letter-spacing: 1px; color: var(--text); }
.m-nav { display: flex; gap: 18px; font-size: 14px; align-items: center; }
.m-nav a.active { font-weight: 700; }
.m-link-btn { background: none; border: none; color: var(--accent); font-size: 14px; cursor: pointer; padding: 0; }
.inline-form { display: inline; }
.m-header {
border-bottom: 1px solid var(--border);
background: var(--card);
}
.m-header .m-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 18px 20px;
}
.m-logo {
font-size: 22px;
font-weight: 800;
letter-spacing: 1px;
color: var(--text);
}
.m-nav {
display: flex;
gap: 18px;
font-size: 14px;
align-items: center;
}
.m-nav a.active {
font-weight: 700;
}
.m-link-btn {
background: none;
border: none;
color: var(--accent);
font-size: 14px;
cursor: pointer;
padding: 0;
}
.m-search input {
padding: 7px 12px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg);
color: var(--text);
font-size: 14px;
width: 140px;
}
.inline-form {
display: inline;
}
.main-layout { padding: 36px 20px; }
.main-layout .content { max-width: 720px; margin: 0 auto; }
.main-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 300px;
gap: 32px;
align-items: start;
max-width: 1080px;
margin: 0 auto;
padding: 36px 20px;
}
.main-layout .content {
min-width: 0;
max-width: 100%;
}
.main-layout .sidebar {
width: auto;
min-width: 0;
}
.post-card, .post-full, .comments-section { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 26px 30px; margin-bottom: 22px; }
.post-card.sticky { box-shadow: inset 3px 0 0 var(--accent); }
.post-title { font-size: 20px; margin-bottom: 8px; line-height: 1.45; }
.post-card .post-title a { color: var(--text); }
.badge { background: var(--accent); color: #fff; font-size: 11px; padding: 2px 7px; border-radius: 3px; margin-right: 6px; vertical-align: 2px; }
.post-meta { color: var(--muted); font-size: 13px; margin-bottom: 12px; }
.post-excerpt { color: var(--muted); }
.post-footer { margin-top: 12px; display: flex; gap: 12px; flex-wrap: wrap; font-size: 14px; }
.read-more { font-weight: 600; }
.post-card,
.post-full,
.comments-section {
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 26px 30px;
margin-bottom: 22px;
}
.post-card.sticky {
box-shadow: inset 3px 0 0 var(--accent);
}
.post-title {
font-size: 20px;
margin-bottom: 8px;
line-height: 1.45;
}
.post-card .post-title a {
color: var(--text);
}
.badge {
background: var(--accent);
color: #fff;
font-size: 11px;
padding: 2px 7px;
border-radius: 3px;
margin-right: 6px;
vertical-align: 2px;
}
.post-meta {
color: var(--muted);
font-size: 13px;
margin-bottom: 12px;
}
.post-excerpt {
color: var(--muted);
}
.post-footer {
margin-top: 12px;
display: flex;
gap: 12px;
flex-wrap: wrap;
font-size: 14px;
}
.read-more {
font-weight: 600;
}
.post-full .post-title { font-size: 28px; }
.post-content { margin-top: 14px; }
.post-content img { max-width: 100%; border-radius: 8px; }
.post-content pre { background: #0d1117; color: #c9d1d9; padding: 16px; border-radius: 8px; overflow-x: auto; margin: 14px 0; }
.post-content code { background: rgba(128,128,128,.15); padding: 2px 6px; border-radius: 4px; font-size: 14px; }
.post-content pre code { background: none; padding: 0; }
.post-content blockquote { border-left: 3px solid var(--accent); padding-left: 16px; color: var(--muted); margin: 14px 0; }
.post-content h1, .post-content h2, .post-content h3 { margin: 20px 0 8px; }
.post-full .post-title {
font-size: 28px;
}
.post-content {
margin-top: 14px;
max-width: 100%;
overflow-wrap: anywhere;
word-break: break-word;
}
.post-content img {
max-width: 100%;
height: auto;
border-radius: 8px;
}
.post-content pre,
.post-content .codeText,
.post-content ol[class^="dp-"] {
background: #0d1117;
color: #c9d1d9;
padding: 16px;
border-radius: 8px;
overflow-x: auto;
max-width: 100%;
margin: 14px 0;
white-space: pre-wrap;
word-break: break-word;
}
.post-content code {
background: rgba(128, 128, 128, 0.15);
padding: 2px 6px;
border-radius: 4px;
font-size: 14px;
word-break: break-all;
overflow-wrap: anywhere;
white-space: break-spaces;
}
.post-content pre code {
display: block;
background: none;
padding: 0;
white-space: pre-wrap;
word-break: break-word;
}
.post-content blockquote {
border-left: 3px solid var(--accent);
padding-left: 16px;
color: var(--muted);
margin: 14px 0;
}
.post-content h1,
.post-content h2,
.post-content h3 {
margin: 20px 0 8px;
}
.post-cover { margin: -26px -30px 18px; border-radius: 12px 12px 0 0; overflow: hidden; }
.post-cover img { width: 100%; display: block; max-height: 380px; object-fit: cover; }
.post-cover {
margin: -26px -30px 18px;
border-radius: 12px 12px 0 0;
overflow: hidden;
}
.post-cover img {
width: 100%;
display: block;
max-height: 380px;
object-fit: cover;
}
.tag-link { color: var(--accent); font-size: 14px; margin-right: 8px; }
.tag-link {
color: var(--accent);
font-size: 14px;
margin-right: 8px;
}
.comments-section { margin-top: 8px; }
.section-title { font-size: 17px; margin-bottom: 14px; }
.comment { padding: 12px 0; border-bottom: 1px solid var(--border); }
.comment:last-of-type { border-bottom: none; }
.comment-head { font-size: 14px; margin-bottom: 4px; }
.comment-time { color: var(--muted); font-size: 13px; }
.comment-body { color: var(--text); font-size: 15px; }
.comment-form { margin-top: 14px; }
.form-row { margin-bottom: 12px; }
.form-row label { display: block; font-size: 14px; margin-bottom: 4px; color: var(--muted); }
.form-row input, .form-row textarea { width: 100%; padding: 10px 14px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg); color: var(--text); font-size: 15px; font-family: inherit; }
.honeypot { position: absolute; left: -9999px; opacity: 0; }
.error { color: var(--accent); font-size: 13px; }
.btn { background: var(--accent); color: #fff; border: none; padding: 10px 24px; border-radius: 8px; font-size: 15px; cursor: pointer; }
.alert { padding: 12px 18px; border-radius: 8px; margin-bottom: 16px; font-size: 14px; }
.alert-success { background: rgba(46,160,67,.12); color: #2ea043; }
.alert-error { background: rgba(248,81,73,.12); color: #f85149; }
.empty { color: var(--muted); text-align: center; padding: 40px 0; }
.comments-section {
margin-top: 8px;
}
.section-title {
font-size: 17px;
margin-bottom: 14px;
}
.comment {
padding: 12px 0;
border-bottom: 1px solid var(--border);
}
.comment:last-of-type {
border-bottom: none;
}
.comment-head {
font-size: 14px;
margin-bottom: 4px;
}
.comment-time {
color: var(--muted);
font-size: 13px;
}
.comment-body {
color: var(--text);
font-size: 15px;
}
.comment-form {
margin-top: 14px;
}
.form-row {
margin-bottom: 12px;
}
.form-row label {
display: block;
font-size: 14px;
margin-bottom: 4px;
color: var(--muted);
}
.form-row input,
.form-row textarea {
width: 100%;
padding: 10px 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg);
color: var(--text);
font-size: 15px;
font-family: inherit;
}
.honeypot {
position: absolute;
left: -9999px;
opacity: 0;
}
.error {
color: var(--accent);
font-size: 13px;
}
.btn {
background: var(--accent);
color: #fff;
border: none;
padding: 10px 24px;
border-radius: 8px;
font-size: 15px;
cursor: pointer;
}
.alert {
padding: 12px 18px;
border-radius: 8px;
margin-bottom: 16px;
font-size: 14px;
}
.alert-success {
background: rgba(46, 160, 67, 0.12);
color: #2ea043;
}
.alert-error {
background: rgba(248, 81, 73, 0.12);
color: #f85149;
}
.alert-warning {
background: rgba(210, 153, 34, 0.12);
color: #9a6700;
}
.empty {
color: var(--muted);
text-align: center;
padding: 40px 0;
}
.list-title { font-size: 24px; margin-bottom: 20px; }
.list-title {
font-size: 24px;
margin-bottom: 20px;
}
.archive-year { font-size: 16px; margin: 16px 0 6px; }
.archive-list { list-style: none; }
.archive-posts { margin: 4px 0 12px 22px; }
.archive-posts time { color: var(--muted); font-size: 13px; }
.archive-year {
font-size: 16px;
margin: 16px 0 6px;
}
.archive-list {
list-style: none;
}
.archive-posts {
margin: 4px 0 12px 22px;
}
.archive-posts time {
color: var(--muted);
font-size: 13px;
}
.pagination { margin: 16px 0; }
.pagination-links { display: flex; gap: 8px; flex-wrap: wrap; }
.pagination-links a, .pagination-links .current, .pagination-links .disabled { padding: 6px 14px; border: 1px solid var(--border); border-radius: 6px; font-size: 14px; background: var(--card); color: var(--text); }
.pagination-links .current { background: var(--accent); color: #fff; border-color: var(--accent); }
.pagination-links .disabled { color: var(--muted); }
.view-mode-switcher {
font-size: 13px;
color: var(--muted);
margin-bottom: 16px;
}
.view-mode-switcher a.active {
font-weight: 700;
color: var(--text);
}
.m-footer { border-top: 1px solid var(--border); padding: 26px 0 40px; margin-top: 30px; text-align: center; color: var(--muted); font-size: 13px; }
.pagination {
width: 100%;
max-width: 100%;
min-width: 0;
margin: 16px 0;
}
.pagination-links {
display: flex;
gap: 6px;
flex-wrap: wrap;
align-items: center;
max-width: 100%;
}
.pagination-info {
color: var(--muted);
font-size: 13px;
margin-right: 6px;
white-space: nowrap;
}
.pagination-links a,
.pagination-links .current,
.pagination-links .disabled {
padding: 6px 14px;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 14px;
background: var(--card);
color: var(--text);
}
.pagination-links .current {
background: var(--accent);
color: #fff;
border-color: var(--accent);
}
.pagination-links .disabled {
color: var(--muted);
}
.tag-cloud.big { line-height: 2.4; }
.links-list { list-style: none; }
.links-list li { padding: 6px 0; }
.link-note { color: var(--muted); font-size: 14px; }
.m-footer {
border-top: 1px solid var(--border);
padding: 26px 0 40px;
margin-top: 30px;
text-align: center;
color: var(--muted);
font-size: 13px;
}
.auth-layout { display: flex; justify-content: center; padding: 50px 20px; }
.auth-box { width: 100%; max-width: 420px; background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 30px; }
.auth-alt { margin-top: 14px; font-size: 14px; color: var(--muted); text-align: center; }
.search-form { margin-bottom: 16px; display: flex; gap: 8px; }
.search-form input { flex: 1; padding: 10px 14px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg); color: var(--text); font-size: 15px; }
.tag-cloud.big {
line-height: 2.4;
}
.links-list {
list-style: none;
}
.links-list li {
padding: 6px 0;
}
.link-note {
color: var(--muted);
font-size: 14px;
}
.auth-layout {
display: flex;
justify-content: center;
padding: 50px 20px;
}
.auth-box {
width: 100%;
max-width: 420px;
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 30px;
}
.auth-alt {
margin-top: 14px;
font-size: 14px;
color: var(--muted);
text-align: center;
}
.search-form {
margin-bottom: 16px;
display: flex;
gap: 8px;
}
.search-form input {
flex: 1;
padding: 10px 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg);
color: var(--text);
font-size: 15px;
}
/* ============ 侧边栏 ============ */
.sidebar {
position: sticky;
top: 20px;
}
.widget {
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 18px 20px;
margin-bottom: 18px;
}
.widget-title {
font-size: 15px;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 2px solid var(--accent);
}
.widget-list {
list-style: none;
}
.widget-list li {
font-size: 14px;
padding: 5px 0;
border-bottom: 1px solid var(--border);
}
/* 社交账号:横向胶囊列表 */
.social-links {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.social-links li {
font-size: 12px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 999px;
padding: 4px 10px;
}
.widget-list li:last-child {
border-bottom: none;
}
.count {
color: var(--muted);
font-size: 13px;
}
.tag-cloud {
line-height: 2;
}
.tag-cloud a {
display: inline-block;
}
.tag-cloud.big {
line-height: 2.4;
}
/* ============ 顶栏搜索(默认 header partial) ============ */
.site-search input {
padding: 7px 12px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg);
color: var(--text);
font-size: 14px;
width: 160px;
}
.link-btn {
background: none;
border: none;
color: var(--accent);
font-size: 15px;
cursor: pointer;
padding: 0;
}
/* ============ 文章正文补充 ============ */
.post-tags {
margin-top: 16px;
}
.post-content ul,
.post-content ol {
padding-left: 24px;
margin: 8px 0;
}
.post-content h4,
.post-content h5,
.post-content h6 {
margin: 16px 0 6px;
}
.post-content table {
border-collapse: collapse;
width: 100%;
margin: 12px 0;
}
.post-content th,
.post-content td {
border: 1px solid var(--border);
padding: 6px 10px;
}
.post-content hr {
border: none;
border-top: 1px solid var(--border);
margin: 18px 0;
}
/* ============ 个人中心 ============ */
.profile-box {
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 24px 28px;
margin-bottom: 20px;
}
.profile-box p {
margin-bottom: 8px;
}
/* ============ 按钮变体 ============ */
.btn-primary {
background: var(--accent);
}
.btn:hover {
opacity: 0.9;
}
/* ============ 会员/支付 ============ */
.plans-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 16px;
}
.plan-card .plan-price {
font-size: 26px;
font-weight: 800;
color: var(--accent);
}
.paid-teaser {
background: rgba(248, 81, 73, 0.08);
border: 1px dashed var(--accent);
border-radius: 8px;
padding: 12px 16px;
color: var(--muted);
font-size: 14px;
margin: 12px 0;
}
/* ============ 移动端适配 ============ */
@media (max-width: 768px) {
.m-header .m-container {
flex-direction: column;
align-items: flex-start;
gap: 12px;
padding: 16px 20px;
}
.m-nav {
flex-wrap: wrap;
gap: 12px;
width: 100%;
}
.m-search {
flex: 1;
min-width: 100%;
}
.m-search input {
width: 100%;
}
.main-layout {
grid-template-columns: minmax(0, 1fr);
padding: 24px 16px;
}
.main-layout .content {
max-width: 100%;
}
.main-layout .sidebar {
width: 100%;
}
.post-card,
.post-full,
.comments-section {
padding: 20px;
}
.post-cover {
margin: -20px -20px 16px;
}
.post-full .post-title {
font-size: 24px;
}
.auth-box {
padding: 24px 20px;
}
.pagination-links {
gap: 6px;
}
.pagination-links a,
.pagination-links .current,
.pagination-links .disabled {
padding: 5px 10px;
}
}
/* TOC */
.toc {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 10px;
padding: 14px 18px;
margin: 0 0 18px;
font-size: 14px;
}
.toc summary {
cursor: pointer;
font-weight: 600;
margin-bottom: 6px;
}
.toc-list {
list-style: none;
}
.toc-list li {
padding: 3px 0;
}
.toc-list a {
color: var(--muted);
}
.toc-list a:hover {
color: var(--accent);
}
.toc-level-3 {
padding-left: 14px;
}
.toc-level-4,
.toc-level-5,
.toc-level-6 {
padding-left: 28px;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
/* Paywall */
.paywall {
border: 1px solid var(--border);
border-radius: 12px;
padding: 28px 22px;
background: var(--bg);
text-align: center;
}
.paywall-cover img {
max-width: 100%;
border-radius: 10px;
margin-bottom: 16px;
}
.paywall-title {
font-size: 22px;
margin: 0 0 10px;
}
.paywall-excerpt {
color: var(--muted);
font-size: 14px;
margin: 0 0 16px;
}
.paywall-price {
font-size: 24px;
font-weight: 800;
color: var(--accent);
margin: 0 0 8px;
}
.paywall-hint {
color: var(--muted);
font-size: 13px;
margin: 0 0 18px;
}
.paywall-actions .btn {
margin: 0 6px;
}
.paid-teaser {
border: 1px dashed var(--border);
background: var(--bg-muted, #f6f6f6);
color: var(--muted);
border-radius: 10px;
padding: 14px 16px;
margin: 14px 0;
font-size: 14px;
}
.paid-teaser .btn {
margin-left: 8px;
}
/* 已解锁的付费内容块 */
.paid-content {
border: 1px solid var(--border);
border-left: 4px solid var(--accent);
background: var(--bg);
border-radius: 10px;
padding: 14px 16px;
margin: 14px 0;
}
.paid-content-label {
font-size: 12px;
color: var(--accent);
font-weight: 600;
margin-bottom: 6px;
}
/* 分类页介绍区 */
.category-intro {
display: flex;
gap: 14px;
align-items: flex-start;
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px 18px;
margin-bottom: 18px;
}
.category-cover {
width: 128px;
height: 84px;
object-fit: cover;
border-radius: 8px;
flex-shrink: 0;
}
.category-description {
margin: 0 0 6px;
font-size: 14px;
color: var(--muted);
line-height: 1.7;
}
.category-meta {
margin: 0;
font-size: 12px;
color: var(--muted);
}
+657 -81
View File
@@ -1,115 +1,691 @@
/* ============ Sablog 经典主题 ============ */
* { margin: 0; padding: 0; box-sizing: border-box; }
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #eef2f5 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==) repeat-x top;
background: #eef2f5
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==)
repeat-x top;
font-family: Verdana, "PingFang SC", "Microsoft YaHei", sans-serif;
font-size: 13px;
color: #333;
line-height: 1.7;
}
a { color: #1c62b0; text-decoration: none; }
a:hover { color: #f60; text-decoration: underline; }
a {
color: #1c62b0;
text-decoration: none;
}
a:hover {
color: #f60;
text-decoration: underline;
}
#outmain { width: 960px; margin: 0 auto; }
#outmain {
width: 960px;
max-width: 100%;
margin: 0 auto;
}
#header { padding: 22px 10px 8px; }
.logo a { font-size: 30px; font-weight: bold; color: #1c62b0; font-family: Georgia, serif; }
.logo .description { color: #888; font-size: 12px; }
#header {
padding: 22px 10px 8px;
}
.logo a {
font-size: 30px;
font-weight: bold;
color: #1c62b0;
font-family: Georgia, serif;
}
.logo .description {
color: #888;
font-size: 12px;
}
#nav { margin-top: 14px; border-top: 1px solid #d8dfe6; border-bottom: 1px solid #d8dfe6; background: #f7f9fb; }
#nav ul { list-style: none; display: flex; flex-wrap: wrap; }
#nav li { border-right: 1px solid #d8dfe6; }
#nav a { display: block; padding: 7px 18px; color: #333; font-weight: bold; font-size: 13px; }
#nav a:hover { background: #1c62b0; color: #fff; text-decoration: none; }
#nav .current_page_item a { background: #1c62b0; color: #fff; }
#nav {
margin-top: 14px;
border-top: 1px solid #d8dfe6;
border-bottom: 1px solid #d8dfe6;
background: #f7f9fb;
}
#nav ul {
list-style: none;
display: flex;
flex-wrap: wrap;
}
#nav li {
border-right: 1px solid #d8dfe6;
}
#nav a {
display: block;
padding: 7px 18px;
color: #333;
font-weight: bold;
font-size: 13px;
}
#nav a:hover {
background: #1c62b0;
color: #fff;
text-decoration: none;
}
#nav .current_page_item a {
background: #1c62b0;
color: #fff;
}
#page { padding: 14px 10px; display: flex; gap: 18px; align-items: flex-start; }
#page {
padding: 14px 10px;
display: grid;
grid-template-columns: minmax(0, 1fr) 280px;
gap: 18px;
align-items: start;
width: 100%;
max-width: 100%;
}
#content { flex: 1; min-width: 0; }
#content {
min-width: 0;
max-width: 100%;
overflow-wrap: anywhere;
}
.post-card, .post-full, .comments-section, .auth-box, .profile-box, .links-list {
.post-card,
.post-full,
.comments-section,
.auth-box,
.profile-box,
.links-list {
background: #fff;
border: 1px solid #d8dfe6;
padding: 18px 20px;
margin-bottom: 16px;
}
.post-card.sticky { border-left: 4px solid #f60; }
.post-title { font-size: 17px; margin-bottom: 6px; line-height: 1.4; }
.post-card .post-title a { color: #1c62b0; }
.badge { background: #f60; color: #fff; font-size: 11px; padding: 1px 5px; margin-right: 5px; }
.post-meta { color: #999; font-size: 12px; margin-bottom: 8px; }
.post-excerpt { color: #555; margin-bottom: 8px; }
.post-footer { font-size: 12px; }
.tag-link { color: #1c62b0; margin-right: 8px; }
.post-card.sticky {
border-left: 4px solid #f60;
}
.post-title {
font-size: 17px;
margin-bottom: 6px;
line-height: 1.4;
}
.post-card .post-title a {
color: #1c62b0;
}
.badge {
background: #f60;
color: #fff;
font-size: 11px;
padding: 1px 5px;
margin-right: 5px;
}
.post-meta {
color: #999;
font-size: 12px;
margin-bottom: 8px;
}
.post-excerpt {
color: #555;
margin-bottom: 8px;
}
.post-footer {
font-size: 12px;
}
.tag-link {
color: #1c62b0;
margin-right: 8px;
}
.post-full .post-title { font-size: 22px; color: #1c62b0; }
.post-content { margin-top: 10px; font-size: 14px; }
.post-content img { max-width: 100%; }
.post-content pre { background: #f4f4f4; border: 1px solid #ddd; padding: 10px; overflow: auto; margin: 10px 0; }
.post-content blockquote { border-left: 4px solid #1c62b0; padding-left: 12px; color: #666; margin: 10px 0; }
.post-content h1, .post-content h2, .post-content h3 { margin: 14px 0 6px; }
.post-full .post-title {
font-size: 22px;
color: #1c62b0;
}
.post-content {
margin-top: 10px;
font-size: 14px;
max-width: 100%;
overflow-wrap: anywhere;
word-break: break-word;
}
.post-content img {
max-width: 100%;
height: auto;
}
.post-content pre,
.post-content .codeText,
.post-content ol[class^="dp-"] {
background: #f4f4f4;
border: 1px solid #ddd;
border-left: 3px solid #b3cbe1;
padding: 10px;
overflow-x: auto;
max-width: 100%;
margin: 10px 0;
white-space: pre-wrap;
word-break: break-word;
}
.post-content code {
font-family: "Courier New", Courier, monospace;
word-break: break-all;
overflow-wrap: anywhere;
white-space: break-spaces;
}
.post-content pre code {
display: block;
background: none;
padding: 0;
white-space: pre-wrap;
word-break: break-word;
}
.post-content .codeText li {
white-space: pre-wrap;
word-break: break-word;
}
.post-content blockquote {
border-left: 4px solid #1c62b0;
padding-left: 12px;
color: #666;
margin: 10px 0;
}
.post-content h1,
.post-content h2,
.post-content h3 {
margin: 14px 0 6px;
}
.post-cover { margin: -18px -20px 12px; }
.post-cover img { width: 100%; display: block; max-height: 320px; object-fit: cover; }
.post-cover {
margin: -18px -20px 12px;
}
.post-cover img {
width: 100%;
display: block;
max-height: 320px;
object-fit: cover;
}
.comment { padding: 10px 0; border-bottom: 1px dashed #ddd; }
.comment-head { font-size: 12px; margin-bottom: 3px; }
.comment-time { color: #999; }
.comment-body { font-size: 13px; }
.comment-form { margin-top: 12px; }
.form-row { margin-bottom: 8px; }
.form-row label { display: block; font-size: 12px; margin-bottom: 3px; }
.form-row input, .form-row textarea { width: 100%; padding: 6px 8px; border: 1px solid #ccc; font-size: 13px; font-family: inherit; }
.honeypot { position: absolute; left: -9999px; opacity: 0; }
.btn { background: #1c62b0; color: #fff; border: none; padding: 7px 18px; font-size: 13px; cursor: pointer; }
.btn:hover { background: #f60; }
.error { color: #c00; font-size: 12px; }
.alert { padding: 8px 12px; margin-bottom: 12px; border: 1px solid; }
.alert-success { background: #e8f5e9; border-color: #a5d6a7; color: #2e7d32; }
.alert-error { background: #fdecea; border-color: #f5c6cb; color: #c62828; }
.empty { color: #999; text-align: center; padding: 20px 0; }
.list-title { font-size: 18px; margin-bottom: 12px; color: #1c62b0; }
.comment {
padding: 10px 0;
border-bottom: 1px dashed #ddd;
}
.comment-head {
font-size: 12px;
margin-bottom: 3px;
}
.comment-time {
color: #999;
}
.comment-body {
font-size: 13px;
}
.comment-form {
margin-top: 12px;
}
.form-row {
margin-bottom: 8px;
}
.form-row label {
display: block;
font-size: 12px;
margin-bottom: 3px;
}
.form-row input,
.form-row textarea {
width: 100%;
padding: 6px 8px;
border: 1px solid #ccc;
font-size: 13px;
font-family: inherit;
}
.honeypot {
position: absolute;
left: -9999px;
opacity: 0;
}
.btn {
background: #1c62b0;
color: #fff;
border: none;
padding: 7px 18px;
font-size: 13px;
cursor: pointer;
}
.btn:hover {
background: #f60;
}
.error {
color: #c00;
font-size: 12px;
}
.alert {
padding: 8px 12px;
margin-bottom: 12px;
border: 1px solid;
}
.alert-success {
background: #e8f5e9;
border-color: #a5d6a7;
color: #2e7d32;
}
.alert-error {
background: #fdecea;
border-color: #f5c6cb;
color: #c62828;
}
.alert-warning {
background: #fff8e1;
border-color: #ffe082;
color: #8a6d00;
}
.empty {
color: #999;
text-align: center;
padding: 20px 0;
}
.list-title {
font-size: 18px;
margin-bottom: 12px;
color: #1c62b0;
}
.view-mode-switcher {
font-size: 12px;
color: #888;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px solid #d8dfe6;
}
.view-mode-switcher a.active {
font-weight: 700;
color: #1c62b0;
}
/* Sidebar */
.sidebar { width: 280px; flex-shrink: 0; }
.sidebar .searchbox { background: #fff; border: 1px solid #d8dfe6; padding: 10px; margin-bottom: 14px; }
.sidebar .searchbox input[type=search] { width: 180px; padding: 4px; border: 1px solid #ccc; }
.sidebar .widget { background: #fff; border: 1px solid #d8dfe6; margin-bottom: 14px; }
.sidebar .widget h3 { background: #f7f9fb; border-bottom: 1px solid #d8dfe6; font-size: 13px; padding: 7px 12px; color: #1c62b0; }
.sidebar .widget ul { list-style: none; padding: 6px 12px; }
.sidebar .widget li { padding: 3px 0; font-size: 12px; border-bottom: 1px dotted #eee; }
.sidebar .tagcloud { padding: 8px 12px; }
.sidebar .tagcloud a { color: #1c62b0; margin-right: 6px; }
.sidebar {
width: auto;
min-width: 0;
}
.sidebar .searchbox {
background: #fff;
border: 1px solid #d8dfe6;
padding: 10px;
margin-bottom: 14px;
}
.sidebar .searchbox input[type="search"] {
width: 180px;
padding: 4px;
border: 1px solid #ccc;
}
.sidebar .widget {
background: #fff;
border: 1px solid #d8dfe6;
margin-bottom: 14px;
}
.sidebar .widget h3 {
background: #f7f9fb;
border-bottom: 1px solid #d8dfe6;
font-size: 13px;
padding: 7px 12px;
color: #1c62b0;
}
.sidebar .widget ul {
list-style: none;
padding: 6px 12px;
}
.sidebar .widget li {
padding: 3px 0;
font-size: 12px;
border-bottom: 1px dotted #eee;
}
.sidebar .tagcloud {
padding: 8px 12px;
}
.sidebar .tagcloud a {
color: #1c62b0;
margin-right: 6px;
}
/* 兜底页面(归档/标签云/友链/评论/登录等使用默认布局) */
.main-layout { display: flex; gap: 18px; align-items: flex-start; padding: 14px 10px; }
.main-layout .content { flex: 1; min-width: 0; }
.main-layout .sidebar { width: 280px; }
.container.main-layout {
max-width: 960px;
margin: 0 auto;
}
.main-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 280px;
gap: 18px;
align-items: start;
padding: 14px 10px;
}
.main-layout .content {
min-width: 0;
max-width: 100%;
}
.main-layout .sidebar {
width: auto;
min-width: 0;
}
.pagination-links { display: flex; gap: 6px; flex-wrap: wrap; margin: 12px 0; }
.pagination-links a, .pagination-links .current, .pagination-links .disabled { padding: 4px 10px; border: 1px solid #d8dfe6; background: #fff; font-size: 12px; }
.pagination-links .current { background: #1c62b0; color: #fff; }
.pagination {
width: 100%;
max-width: 100%;
min-width: 0;
margin: 12px 0;
}
.pagination-links {
display: flex;
gap: 4px;
flex-wrap: wrap;
align-items: center;
max-width: 100%;
margin: 12px 0;
}
.pagination-info {
color: #888;
font-size: 12px;
margin-right: 6px;
white-space: nowrap;
}
.pagination-links a,
.pagination-links .current,
.pagination-links .disabled {
padding: 2px 7px;
border: 1px solid #d8dfe6;
background: #fff;
font-size: 12px;
line-height: 1.4;
}
.pagination-links .current {
background: #1c62b0;
color: #fff;
}
#footer { clear: both; text-align: center; padding: 14px 0 24px; color: #888; font-size: 12px; }
#footer a { color: #1c62b0; }
#footer {
clear: both;
text-align: center;
padding: 14px 0 24px;
color: #888;
font-size: 12px;
}
#footer a {
color: #1c62b0;
}
.archive-year { font-size: 15px; margin: 12px 0 4px; color: #1c62b0; }
.archive-list { list-style: none; }
.archive-posts { margin: 4px 0 10px 20px; }
.archive-year {
font-size: 15px;
margin: 12px 0 4px;
color: #1c62b0;
}
.archive-list {
list-style: none;
}
.archive-posts {
margin: 4px 0 10px 20px;
}
.links-list { list-style: none; }
.links-list li { padding: 5px 0; }
.link-note { color: #999; }
.links-list {
list-style: none;
}
.links-list li {
padding: 5px 0;
}
.link-note {
color: #999;
}
.tag-cloud.big { line-height: 2.4; }
.tag-cloud.big {
line-height: 2.4;
}
.auth-layout { display: flex; justify-content: center; padding: 30px 10px; }
.auth-box { width: 100%; max-width: 420px; }
.auth-alt { margin-top: 12px; font-size: 12px; }
.search-form { margin-bottom: 12px; }
.search-form input { padding: 5px; border: 1px solid #ccc; width: 220px; }
.auth-layout {
display: flex;
justify-content: center;
padding: 30px 10px;
}
.auth-box {
width: 100%;
max-width: 420px;
}
.auth-alt {
margin-top: 12px;
font-size: 12px;
}
.search-form {
margin-bottom: 12px;
}
.search-form input {
padding: 5px;
border: 1px solid #ccc;
width: 220px;
}
.inline-form { display: inline; }
.link-btn { background: none; border: none; color: #1c62b0; cursor: pointer; font-size: 13px; padding: 0; font-weight: bold; }
.inline-form {
display: inline;
}
.link-btn {
background: none;
border: none;
color: #1c62b0;
cursor: pointer;
font-size: 13px;
padding: 0;
font-weight: bold;
}
/* ============ 移动端适配 ============ */
@media (max-width: 768px) {
#outmain {
width: 100%;
}
#header {
padding: 14px 10px;
}
.logo a {
font-size: 24px;
}
#nav ul {
flex-wrap: wrap;
}
#nav a {
padding: 8px 12px;
font-size: 13px;
}
#page {
grid-template-columns: minmax(0, 1fr);
}
#content {
width: 100%;
}
.sidebar {
width: 100%;
}
.main-layout {
grid-template-columns: minmax(0, 1fr);
}
.main-layout .content {
width: 100%;
}
.main-layout .sidebar {
width: 100%;
}
.sidebar .searchbox input[type="search"] {
width: 70%;
}
.post-card,
.post-full,
.comments-section {
padding: 14px 16px;
}
.post-cover {
margin: -14px -16px 12px;
}
.post-full .post-title {
font-size: 22px;
}
.archive-posts {
margin-left: 10px;
}
}
/* TOC */
.toc {
background: #f7f9fb;
border: 1px solid #d8dfe6;
padding: 10px 14px;
margin: 0 0 14px;
font-size: 13px;
}
.toc summary {
cursor: pointer;
font-weight: bold;
margin-bottom: 4px;
}
.toc-list {
list-style: none;
}
.toc-list li {
padding: 2px 0;
}
.toc-level-3 {
padding-left: 14px;
}
.toc-level-4,
.toc-level-5,
.toc-level-6 {
padding-left: 28px;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
/* Paywall */
.paywall {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 24px 20px;
background: #fafafa;
text-align: center;
}
.paywall-cover img {
max-width: 100%;
border-radius: 6px;
margin-bottom: 14px;
}
.paywall-title {
font-size: 20px;
margin: 0 0 10px;
}
.paywall-excerpt {
color: #666;
font-size: 14px;
margin: 0 0 14px;
}
.paywall-price {
font-size: 22px;
font-weight: 800;
color: #f60;
margin: 0 0 8px;
}
.paywall-hint {
color: #888;
font-size: 13px;
margin: 0 0 16px;
}
.paywall-actions .btn {
margin: 0 6px;
}
.paid-teaser {
border: 1px dashed #f5c6cb;
background: #fdecea;
color: #c62828;
border-radius: 6px;
padding: 12px 14px;
margin: 12px 0;
font-size: 14px;
}
.paid-teaser .btn {
margin-left: 8px;
}
/* 阅读全文链接 */
.read-more {
display: inline-block;
margin-top: 8px;
font-size: 13px;
color: #1c62b0;
}
.read-more:hover {
color: #f60;
}
/* 社交账号:横向胶囊列表 */
.social-links {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 0;
padding: 0;
}
.social-links li {
font-size: 12px;
background: #f7f9fb;
border: 1px solid #d8dfe6;
border-radius: 999px;
padding: 3px 10px;
}
/* 已解锁的付费内容块 */
.paid-content {
border: 1px dashed #f5c6cb;
border-left: 4px solid #f60;
background: #fff8f0;
border-radius: 6px;
padding: 12px 14px;
margin: 12px 0;
}
.paid-content-label {
font-size: 12px;
color: #f60;
font-weight: 700;
margin-bottom: 6px;
}
/* 分类页介绍区 */
.category-intro {
display: flex;
gap: 14px;
align-items: flex-start;
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 14px 16px;
margin-bottom: 16px;
}
.category-cover {
width: 120px;
height: 80px;
object-fit: cover;
border-radius: 6px;
flex-shrink: 0;
}
.category-description {
margin: 0 0 6px;
font-size: 14px;
color: #555;
line-height: 1.7;
}
.category-meta {
margin: 0;
font-size: 12px;
color: #999;
}
@@ -9,6 +9,18 @@
{{ $environmentName }}
</x-filament::badge>
<x-filament::button
tag="a"
href="{{ url('/') }}"
target="_blank"
rel="noopener noreferrer"
size="sm"
color="gray"
icon="heroicon-o-globe-alt"
>
网站
</x-filament::button>
<x-filament::button
tag="a"
:href="$createPostUrl"
@@ -0,0 +1,53 @@
<x-filament-panels::page>
<x-filament::section heading="当前语言">
<dl class="grid gap-3 sm:grid-cols-3">
<div>
<dt class="text-sm text-gray-500 dark:text-gray-400">站点语言</dt>
<dd class="mt-1 text-lg font-medium">{{ $this->info['locale'] }}</dd>
</div>
<div>
<dt class="text-sm text-gray-500 dark:text-gray-400">回退语言</dt>
<dd class="mt-1 text-lg font-medium">{{ $this->info['fallback'] }}</dd>
</div>
<div>
<dt class="text-sm text-gray-500 dark:text-gray-400">假数据语言</dt>
<dd class="mt-1 text-lg font-medium">{{ $this->info['faker'] }}</dd>
</div>
</dl>
<p class="mt-4 text-sm text-gray-500 dark:text-gray-400">
改语言请编辑 <code>.env</code> 里的 <code>APP_LOCALE</code>,后台菜单文案本身是中文写死的。
</p>
</x-filament::section>
<x-filament::section heading="文章多语言" class="mt-6">
<x-filament::badge color="warning">未开通</x-filament::badge>
<p class="mt-3 text-sm text-gray-600 dark:text-gray-300">
现在每篇文章只有一份正文,没有中英两个版本可以切换。这个页面先让你看到现状,以后如果做文章翻译,会在这里列出语言和对应文章。
</p>
</x-filament::section>
<div class="mt-6 grid gap-6 lg:grid-cols-2">
<x-filament::section heading="站点语言包">
@if(count($this->info['app_files']) === 0)
<p class="text-sm text-gray-500 dark:text-gray-400">还没有站点自己的语言文件。</p>
@else
<div class="flex flex-wrap gap-2">
@foreach($this->info['app_files'] as $locale)
<x-filament::badge>{{ $locale }}</x-filament::badge>
@endforeach
</div>
@endif
</x-filament::section>
<x-filament::section heading="Filament 后台语言包">
<p class="mb-3 text-sm text-gray-500 dark:text-gray-400">
这些是后台组件自带的翻译,不是文章内容翻译。
</p>
<div class="flex flex-wrap gap-2">
@foreach($this->info['filament_locales'] as $locale)
<x-filament::badge color="gray">{{ $locale }}</x-filament::badge>
@endforeach
</div>
</x-filament::section>
</div>
</x-filament-panels::page>
@@ -0,0 +1,64 @@
<x-filament-panels::page>
<div class="grid gap-4 md:grid-cols-4">
<x-filament::section>
<div class="text-sm text-gray-500 dark:text-gray-400">当前缓存页</div>
<div class="mt-1 text-2xl font-semibold">{{ $this->stats['count'] }} 条</div>
</x-filament::section>
<x-filament::section>
<div class="text-sm text-gray-500 dark:text-gray-400">命中率</div>
<div class="mt-1 text-2xl font-semibold">{{ $this->stats['rate'] }}%</div>
</x-filament::section>
<x-filament::section>
<div class="text-sm text-gray-500 dark:text-gray-400">命中 / 未命中</div>
<div class="mt-1 text-2xl font-semibold">{{ $this->stats['hits'] }} / {{ $this->stats['misses'] }}</div>
</x-filament::section>
<x-filament::section>
<div class="text-sm text-gray-500 dark:text-gray-400">过期时间</div>
<div class="mt-1 text-2xl font-semibold">{{ $this->stats['ttl'] }} 秒</div>
</x-filament::section>
</div>
<x-filament::section heading="已缓存的前台页面" class="mt-6">
<p class="mb-4 text-sm text-gray-500 dark:text-gray-400">
只缓存未登录访客打开的页面。登录后看前台不会进缓存,所以这里可能暂时是空的。
</p>
@if(count($this->entries) === 0)
<p class="text-sm text-gray-500 dark:text-gray-400">当前没有前台页面缓存。</p>
@else
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-gray-200 text-left dark:border-white/10">
<th class="py-2 pr-4 font-medium">地址</th>
<th class="py-2 pr-4 font-medium">缓存时间</th>
<th class="py-2 font-medium">操作</th>
</tr>
</thead>
<tbody>
@foreach($this->entries as $entry)
<tr class="border-b border-gray-100 dark:border-white/5" wire:key="cache-{{ $entry['key'] }}">
<td class="py-2 pr-4">
<a href="{{ $entry['url'] }}" target="_blank" rel="noopener noreferrer" class="text-primary-600 hover:underline">
{{ $entry['url'] }}
</a>
</td>
<td class="py-2 pr-4 whitespace-nowrap">{{ $entry['cached_at'] ?: '—' }}</td>
<td class="py-2">
<x-filament::button
size="xs"
color="danger"
wire:click="forgetEntry('{{ $entry['key'] }}')"
wire:confirm="删除这条缓存?"
>
删除
</x-filament::button>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
</x-filament::section>
</x-filament-panels::page>
+2
View File
@@ -11,6 +11,8 @@
<div class="alert alert-error">{{ session('error') }}</div>
@endif
@include('partials.view-mode-switcher')
@forelse($posts as $post)
@include('partials.post-card', ['post' => $post])
@empty
+2
View File
@@ -24,6 +24,8 @@
@endif
@include('partials.view-mode-switcher')
@forelse($posts as $post)
@include('partials.post-card', ['post' => $post])
@empty
@@ -0,0 +1,16 @@
@php
$asList = $asList ?? false;
$feedLinks = [
['label' => 'RSS', 'url' => route('feed.rss')],
['label' => 'Sitemap', 'url' => route('feed.sitemap')],
['label' => 'llms.txt', 'url' => route('feed.llms')],
];
@endphp
@foreach($feedLinks as $feedLink)
@if($asList)
<li><a href="{{ $feedLink['url'] }}" target="_blank" rel="noopener noreferrer">{{ $feedLink['label'] }}</a></li>
@else
@if(! $loop->first) · @endif
<a href="{{ $feedLink['url'] }}" target="_blank" rel="noopener noreferrer">{{ $feedLink['label'] }}</a>
@endif
@endforeach
@@ -7,6 +7,7 @@
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">{{ $siteIcp }}</a>
@endif
</p>
<p>@include('partials.feed-links')</p>
<p class="powered">Powered by <a href="https://laravel.com" target="_blank" rel="noopener">Laravel</a> &amp; <a href="https://filamentphp.com" target="_blank" rel="noopener">Filament</a></p>
</div>
</footer>
+1
View File
@@ -12,6 +12,7 @@
<meta name="generator" content="LaraLog">
<link rel="canonical" href="{{ url()->current() }}">
<link rel="alternate" type="application/rss+xml" title="{{ $siteName }}" href="{{ route('feed.rss') }}">
<link rel="alternate" type="text/plain" title="llms.txt" href="{{ route('feed.llms') }}">
<link rel="stylesheet" href="{{ asset('css/blog.css') }}">
@stack('head')
@stack('theme:head')
@@ -12,6 +12,7 @@
<a href="{{ route('tags') }}" class="{{ request()->routeIs('tags*') ? 'active' : '' }}">标签</a>
<a href="{{ route('links') }}" class="{{ request()->routeIs('links') ? 'active' : '' }}">友链</a>
<a href="{{ route('comments') }}" class="{{ request()->routeIs('comments') ? 'active' : '' }}">评论</a>
@include('partials.feed-links')
<form class="site-search" action="{{ route('search') }}" method="get">
<input type="search" name="q" placeholder="搜索..." value="{{ request('q', request('keyword', '')) }}">
</form>
@@ -1,13 +1,13 @@
@if ($paginator->hasPages())
<nav class="pagination-links" role="navigation">
{{-- Previous --}}
<nav class="pagination-links" role="navigation" aria-label="分页">
<span class="pagination-info">第 {{ $paginator->currentPage() }} / {{ $paginator->lastPage() }} 页</span>
@if ($paginator->onFirstPage())
<span class="disabled">&laquo; 上一页</span>
<span class="disabled">&laquo;</span>
@else
<a href="{{ $paginator->previousPageUrl() }}" rel="prev">&laquo; 上一页</a>
<a href="{{ $paginator->previousPageUrl() }}" rel="prev">&laquo;</a>
@endif
{{-- Numbers --}}
@foreach ($elements as $element)
@if (is_string($element))
<span class="disabled">{{ $element }}</span>
@@ -23,11 +23,10 @@
@endif
@endforeach
{{-- Next --}}
@if ($paginator->hasMorePages())
<a href="{{ $paginator->nextPageUrl() }}" rel="next">下一页 &raquo;</a>
<a href="{{ $paginator->nextPageUrl() }}" rel="next">&raquo;</a>
@else
<span class="disabled">下一页 &raquo;</span>
<span class="disabled">&raquo;</span>
@endif
</nav>
@endif
+17 -5
View File
@@ -1,11 +1,17 @@
<article class="post-card {{ $post->is_sticky ? 'sticky' : '' }}">
@php
$viewMode = $homeViewMode ?? \App\Blog\Support\HomeViewMode::LIST;
$showFull = $viewMode === \App\Blog\Support\HomeViewMode::FULL
&& blank($post->read_password)
&& empty(($post->meta ?? [])['members_only']);
@endphp
<article class="post-card {{ $post->isPinned() ? 'sticky' : '' }} post-card-{{ $viewMode }}">
@if($url = $post->coverImageUrl())
<a href="{{ route('posts.show', $post->id) }}" class="post-cover">
<img src="{{ $url }}" alt="{{ $post->title }}" loading="lazy">
</a>
@endif
<h2 class="post-title">
@if($post->is_sticky)<span class="badge">置顶</span>@endif
@if($label = $post->stickyLabel())<span class="badge">{{ $label }}</span>@endif
<a href="{{ route('posts.show', $post->id) }}">{{ $post->title }}</a>
</h2>
<div class="post-meta">
@@ -17,9 +23,15 @@
<span>· 阅读 {{ $post->views }}</span>
<span>· 评论 {{ $post->comment_count }}</span>
</div>
<div class="post-excerpt">
{{ Str::limit(strip_tags($post->excerpt_or_fallback), 200) }}
</div>
@if($showFull)
<div class="post-content">
{!! app(\App\Blog\Services\PostContentRenderer::class)->render($post) !!}
</div>
@else
<div class="post-excerpt">
{{ \Illuminate\Support\Str::limit($post->excerpt_or_fallback, 200) }}
</div>
@endif
<div class="post-footer">
<a href="{{ route('posts.show', $post->id) }}" class="read-more">阅读全文 &raquo;</a>
@foreach($post->tags as $tag)
@@ -0,0 +1,5 @@
@if(! empty($isPreview))
<div class="alert alert-warning">
预览中:这篇文章还没发布,访客打不开。只有管理员和编辑能看到这个页面。
</div>
@endif
@@ -0,0 +1,9 @@
@php
$viewMode = $homeViewMode ?? \App\Blog\Support\HomeViewMode::LIST;
@endphp
<div class="view-mode-switcher">
浏览模式:
<a href="{{ \App\Blog\Support\HomeViewMode::url(\App\Blog\Support\HomeViewMode::FULL) }}" class="{{ $viewMode === \App\Blog\Support\HomeViewMode::FULL ? 'active' : '' }}">标准</a>
<span>|</span>
<a href="{{ \App\Blog\Support\HomeViewMode::url(\App\Blog\Support\HomeViewMode::LIST) }}" class="{{ $viewMode === \App\Blog\Support\HomeViewMode::LIST ? 'active' : '' }}">列表</a>
</div>
+2
View File
@@ -10,6 +10,8 @@
<button type="submit" class="btn">搜索</button>
</form>
@include('partials.view-mode-switcher')
@forelse($posts as $post)
@include('partials.post-card', ['post' => $post])
@empty
+6 -1
View File
@@ -14,6 +14,7 @@
@if(session('error'))
<div class="alert alert-error">{{ session('error') }}</div>
@endif
@include('partials.post-preview-banner')
<article class="post-full">
@if(! empty($toc))
@@ -37,7 +38,11 @@
@endif
<h1 class="post-title">{{ $post->title }}</h1>
<div class="post-meta">
<time datetime="{{ $post->published_at->toIso8601String() }}">{{ $post->published_at->format(blog_setting('normaltime', 'Y-m-d H:i')) }}</time>
@if($post->published_at)
<time datetime="{{ $post->published_at->toIso8601String() }}">{{ $post->published_at->format(blog_setting('normaltime', 'Y-m-d H:i')) }}</time>
@else
<span>未发布</span>
@endif
@if($post->author)<span>· {{ $post->author->name }}</span>@endif
@if($post->category)
<span>· <a href="{{ route('category.show', $post->category->id) }}">{{ $post->category->name }}</a></span>
+2
View File
@@ -6,6 +6,8 @@
<main class="content">
<h1 class="list-title">标签:{{ $tag->name }}</h1>
@include('partials.view-mode-switcher')
@forelse($posts as $post)
@include('partials.post-card', ['post' => $post])
@empty
+16 -8
View File
@@ -10,10 +10,14 @@ use App\Blog\Controllers\FeedController;
use App\Blog\Controllers\HomeController;
use App\Blog\Controllers\LegacyController;
use App\Blog\Controllers\LinkController;
use App\Blog\Controllers\LlmsController;
use App\Blog\Controllers\PostController;
use App\Blog\Controllers\SearchController;
use App\Blog\Controllers\TagController;
use App\Blog\Controllers\ThemeAssetController;
use App\Models\Category;
use App\Models\Post;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Facades\Route;
/*
@@ -33,18 +37,22 @@ Route::get('/', function () {
// sablog 首页分页格式:/index-{page}.shtml
Route::get('/index-{page}.shtml', function (string $page) {
\Illuminate\Pagination\Paginator::currentPageResolver(fn () => max(1, (int) $page));
Paginator::currentPageResolver(fn () => max(1, (int) $page));
return app(HomeController::class)->index();
})->whereNumber('page');
Route::get('/posts/{slug}.md', [LlmsController::class, 'postMarkdown'])
->where('slug', '[^/]+')
->name('geo.post.markdown');
Route::get('/posts/{slug}.shtml', function (string $slug) {
$post = \App\Models\Post::query()->where('slug', $slug)->orWhere('id', (int) $slug)->first();
$post = Post::query()->where('slug', $slug)->orWhere('id', (int) $slug)->first();
return $post ? redirect()->route('posts.show', $post->id, 301) : abort(404);
})->where('slug', '[^/]+');
Route::get('/posts/{slug}', function (string $slug) {
$post = \App\Models\Post::query()->where('slug', $slug)->orWhere('id', (int) $slug)->first();
$post = Post::query()->where('slug', $slug)->orWhere('id', (int) $slug)->first();
return $post ? redirect()->route('posts.show', $post->id, 301) : abort(404);
})->where('slug', '[^/]+');
@@ -55,16 +63,15 @@ Route::get('/show-{id}-{page}.shtml', [PostController::class, 'show'])->whereNum
Route::post('/posts/{post}/comments', [CommentController::class, 'store'])->name('comments.store');
// 浏览量统计(独立端点,避开整页缓存,保证实时)
Route::get('/track-view/{post}', fn (\App\Models\Post $post) => response($post->increment('views') ? '' : '', 204))->name('track-view');
Route::get('/track-view/{post}', fn (Post $post) => response($post->increment('views') ? '' : '', 204))->name('track-view');
Route::get('/category/{slug}.shtml', function (string $slug) {
$category = \App\Models\Category::query()->where('slug', $slug)->orWhere('id', (int) $slug)->first();
$category = Category::query()->where('slug', $slug)->orWhere('id', (int) $slug)->first();
return $category ? redirect()->route('category.show', $category->id, 301) : abort(404);
})->where('slug', '[^/]+');
Route::get('/category/{slug}', function (string $slug) {
$category = \App\Models\Category::query()->where('slug', $slug)->orWhere('id', (int) $slug)->first();
$category = Category::query()->where('slug', $slug)->orWhere('id', (int) $slug)->first();
return $category ? redirect()->route('category.show', $category->id, 301) : abort(404);
})->where('slug', '[^/]+');
@@ -120,7 +127,7 @@ Route::get('/profile', fn () => redirect()->route('profile', [], 301))->middlewa
/*
|--------------------------------------------------------------------------
| 稳定路径:RSS / Sitemap / Robots
| 稳定路径:RSS / Sitemap / Robots / llms.txt
|--------------------------------------------------------------------------
*/
@@ -128,6 +135,7 @@ Route::get('/rss.xml', [FeedController::class, 'rss'])->name('feed.rss');
Route::get('/rss.php', [FeedController::class, 'rss']);
Route::get('/sitemap.xml', [FeedController::class, 'sitemap'])->name('feed.sitemap');
Route::get('/robots.txt', [FeedController::class, 'robots'])->name('robots');
Route::get('/llms.txt', [LlmsController::class, 'index'])->name('feed.llms');
/*
|--------------------------------------------------------------------------
+39 -7
View File
@@ -2,8 +2,11 @@
namespace Tests\Feature;
use App\Blog\Support\PluginManager;
use App\Filament\Support\AdminEnvironment;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Plugins\Demo\HelloWorld\ServiceProvider;
use Tests\TestCase;
class AdminPagesTest extends TestCase
@@ -32,12 +35,40 @@ class AdminPagesTest extends TestCase
public function test_post_resource_loads(): void
{
$this->actingAs($this->admin)->get('/admin/posts')->assertOk();
$this->actingAs($this->admin)->get('/admin/posts/create')->assertOk();
$this->actingAs($this->admin)->get('/admin/posts/create')
->assertOk()
->assertSee('Slug(留空自动生成)', false)
->assertSee('封面图', false)
->assertSee('ll-media-picker-preview', false)
->assertSee('评论状态', false)
->assertSee('访问设置', false)
->assertSee('会员专享', false)
->assertSee('价格', false)
->assertSee('置顶头条', false)
->assertSee('轻置顶', false);
}
public function test_settings_page_loads(): void
{
$this->actingAs($this->admin)->get('/admin/blog-settings')->assertOk();
$this->actingAs($this->admin)
->get('/admin/blog-settings')
->assertOk()
->assertSee('首页浏览模式', false);
}
public function test_page_cache_and_language_pages_load(): void
{
$this->actingAs($this->admin)
->get('/admin/page-cache')
->assertOk()
->assertSee('页面缓存', false)
->assertSee('已缓存的前台页面', false);
$this->actingAs($this->admin)
->get('/admin/languages')
->assertOk()
->assertSee('文章多语言', false)
->assertSee('未开通', false);
}
public function test_comment_resource_loads(): void
@@ -77,8 +108,8 @@ class AdminPagesTest extends TestCase
$original = app()->environment();
app()['env'] = 'production';
$this->assertSame('prod', \App\Filament\Support\AdminEnvironment::name());
$this->assertSame('danger', \App\Filament\Support\AdminEnvironment::color());
$this->assertSame('prod', AdminEnvironment::name());
$this->assertSame('danger', AdminEnvironment::color());
app()['env'] = $original;
}
@@ -88,16 +119,17 @@ class AdminPagesTest extends TestCase
$this->actingAs($this->admin)
->get('/admin')
->assertOk()
->assertSee(\App\Filament\Support\AdminEnvironment::name(), false)
->assertSee(AdminEnvironment::name(), false)
->assertSee('写文章', false)
->assertSee('网站', false)
->assertSee('/admin/posts/create', false);
}
public function test_hello_world_plugin_pages_load_when_enabled(): void
{
$manager = app(\App\Blog\Support\PluginManager::class);
$manager = app(PluginManager::class);
$manager->enable('demo.hello-world');
\Plugins\Demo\HelloWorld\ServiceProvider::bootPlugin($manager);
ServiceProvider::bootPlugin($manager);
$this->actingAs($this->admin)->get('/admin')->assertOk();
$this->actingAs($this->admin)->get('/admin/posts/create')->assertOk();
+72
View File
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Tests\Feature;
use App\Filament\Resources\Posts\Pages\EditPost;
use App\Filament\Resources\Posts\RelationManagers\CommentsRelationManager;
use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\File;
use Livewire\Livewire;
use Tests\TestCase;
class FilamentV5CompatTest extends TestCase
{
use RefreshDatabase;
public function test_no_legacy_filament_table_action_imports(): void
{
$legacy = [];
foreach ([app_path(), base_path('plugins')] as $root) {
foreach (File::allFiles($root) as $file) {
if ($file->getExtension() !== 'php') {
continue;
}
$path = $file->getPathname();
if (str_contains(File::get($path), 'Filament\\Tables\\Actions\\')) {
$legacy[] = $path;
}
}
}
$this->assertSame(
[],
$legacy,
'Filament v5 table actions must use Filament\\Actions, not Filament\\Tables\\Actions'
);
}
public function test_edit_post_boots_comments_relation_manager(): void
{
$this->seed();
$admin = User::query()->where('email', 'admin@laralog.test')->firstOrFail();
$this->actingAs($admin);
$post = Post::create([
'user_id' => $admin->id,
'title' => '评论关系测试',
'slug' => 'comment-relation-test',
'content' => '内容',
'content_format' => 'markdown',
'status' => 'published',
'published_at' => now(),
]);
$this->get('/admin/posts/'.$post->id.'/edit')->assertOk();
Livewire::test(EditPost::class, ['record' => $post->getRouteKey()])
->assertSuccessful();
// The comments table only instantiates here. A GET of the edit page does not catch a bad Action import.
Livewire::test(CommentsRelationManager::class, [
'ownerRecord' => $post,
'pageClass' => EditPost::class,
])->assertSuccessful();
}
}
+78
View File
@@ -0,0 +1,78 @@
<?php
namespace Tests\Feature;
use App\Models\Category;
use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class HomeViewModeTest extends TestCase
{
use RefreshDatabase;
private Post $post;
protected function setUp(): void
{
parent::setUp();
$this->seed();
$user = User::create(['name' => '作者', 'email' => 'view@test.com', 'password' => bcrypt('x')]);
$category = Category::create(['name' => '技术', 'slug' => 'tech-view']);
$this->post = Post::create([
'user_id' => $user->id,
'category_id' => $category->id,
'title' => '浏览模式测试文',
'slug' => 'view-mode-test',
'excerpt' => '列表摘要文字',
'content' => '这是完整正文内容 FULL_BODY_ONLY_TOKEN',
'content_format' => 'markdown',
'status' => 'published',
'published_at' => now(),
]);
}
public function test_list_mode_shows_excerpt_not_full_body(): void
{
$this->get('/?view=list')
->assertOk()
->assertSee('浏览模式', false)
->assertSee('列表摘要文字', false)
->assertDontSee('FULL_BODY_ONLY_TOKEN', false);
}
public function test_full_mode_shows_article_body(): void
{
$this->get('/?view=full')
->assertOk()
->assertSee('浏览模式', false)
->assertSee('FULL_BODY_ONLY_TOKEN', false);
}
public function test_compact_pagination_shows_page_info(): void
{
$userId = $this->post->user_id;
$categoryId = $this->post->category_id;
for ($index = 1; $index <= 12; $index++) {
Post::create([
'user_id' => $userId,
'category_id' => $categoryId,
'title' => '分页文章 '.$index,
'slug' => 'pager-'.$index,
'content' => '正文 '.$index,
'content_format' => 'markdown',
'status' => 'published',
'published_at' => now()->subMinutes($index),
]);
}
$this->get('/')
->assertOk()
->assertSee('第 1 /', false)
->assertSee('页', false);
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
namespace Tests\Feature;
use App\Models\Category;
use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ImportedContentTest extends TestCase
{
use RefreshDatabase;
private Post $post;
protected function setUp(): void
{
parent::setUp();
$this->seed();
$user = User::create(['name' => '作者', 'email' => 'import-html@test.com', 'password' => bcrypt('x')]);
$category = Category::create(['name' => '运维', 'slug' => 'ops']);
$html = '<p>&nbsp;不多说,上代码:</p>'
.'<div class="codeText"><div class="codeHead">XML/HTML代码</div>'
.'<ol class="dp-xml">'
.'<li>docker&nbsp;run -d\\</li>'
.'<li>&nbsp; --restart always \\</li>'
.'<li>&nbsp; --name derper</li>'
.'</ol></div>'
.'<div><div>curl -fsSL example.com \\</div><div>&nbsp; | sh</div></div>';
$this->post = Post::create([
'user_id' => $user->id,
'category_id' => $category->id,
'title' => '还是得derp',
'slug' => 'derp-import',
'excerpt' => '',
'content' => $html,
'content_format' => 'html',
'status' => 'published',
'published_at' => now(),
]);
}
public function test_show_page_hides_nbsp_and_wraps_code(): void
{
$this->get('/show-'.$this->post->id.'.shtml')
->assertOk()
->assertDontSee('&nbsp;', false)
->assertDontSee('codeText', false)
->assertSee('<pre', false)
->assertSee('docker run', false)
->assertSee('language-bash', false);
}
public function test_homepage_excerpt_does_not_show_nbsp(): void
{
$this->get('/')
->assertOk()
->assertSee('不多说,上代码', false)
->assertDontSee('&nbsp;', false);
}
}
+4 -2
View File
@@ -4,10 +4,10 @@ namespace Tests\Feature;
use App\Models\Category;
use App\Models\Comment;
use App\Models\Post;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Spatie\Tags\Tag;
use Tests\TestCase;
@@ -23,7 +23,7 @@ class LegacyRoutesTest extends TestCase
$user = User::create(['name' => '作者', 'email' => 'author@test.com', 'password' => bcrypt('x')]);
$category = Category::create(['name' => '技术', 'slug' => 'tech']);
\Illuminate\Support\Facades\DB::table('posts')->insert([
DB::table('posts')->insert([
'id' => 12,
'user_id' => $user->id,
'category_id' => $category->id,
@@ -93,6 +93,8 @@ class LegacyRoutesTest extends TestCase
$this->get('/rss.xml')->assertOk();
$this->get('/sitemap.xml')->assertOk();
$this->get('/robots.txt')->assertOk();
$this->get('/llms.txt')->assertOk();
$this->get('/posts/legacy-post.md')->assertOk();
}
public function test_missing_records_return_404(): void
+110
View File
@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace Tests\Feature;
use App\Blog\Support\PageCacheMiddleware;
use App\Blog\Support\ThemeManager;
use App\Models\Category;
use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class LlmsFeedTest extends TestCase
{
use RefreshDatabase;
private User $author;
private Category $category;
protected function setUp(): void
{
parent::setUp();
$this->seed();
$this->author = User::query()->where('email', 'admin@laralog.test')->firstOrFail();
$this->category = Category::create([
'name' => '索引',
'slug' => 'llms-cat',
]);
}
public function test_llms_txt_lists_new_published_posts_and_skips_drafts(): void
{
$published = $this->makePost('给 Agent 看的新文章', 'agent-new-post', 'published');
$this->makePost('还没发布', 'agent-draft', 'draft');
$this->get('/llms.txt')
->assertOk()
->assertHeader('Content-Type', 'text/plain; charset=utf-8')
->assertSee('给 Agent 看的新文章', false)
->assertSee('/posts/agent-new-post.md', false)
->assertSee($published->url, false)
->assertDontSee('还没发布', false);
}
public function test_markdown_export_is_live_and_hides_restricted_posts(): void
{
$this->makePost('Markdown 导出', 'md-export', 'published', '这里是正文。');
$this->get('/posts/md-export.md')
->assertOk()
->assertHeader('Content-Type', 'text/markdown; charset=utf-8')
->assertSee('# Markdown 导出', false)
->assertSee('这里是正文。', false);
$this->makePost('会员专享', 'members-only-md', 'published', '秘密', [
'members_only' => true,
]);
$this->get('/posts/members-only-md.md')->assertNotFound();
$this->get('/llms.txt')->assertDontSee('会员专享', false);
}
public function test_llms_and_feeds_are_not_page_cached(): void
{
$this->makePost('缓存外的文章', 'no-cache-llms', 'published');
PageCacheMiddleware::flush();
$this->get('/llms.txt')->assertOk();
$this->get('/rss.xml')->assertOk();
$this->get('/posts/no-cache-llms.md')->assertOk();
$this->assertSame(0, PageCacheMiddleware::count());
}
public function test_frontend_feed_links_open_in_a_new_tab(): void
{
app(ThemeManager::class)->activate('sablog');
$this->get('/')
->assertOk()
->assertSee('target="_blank"', false)
->assertSee('rel="noopener noreferrer"', false)
->assertSee(route('feed.rss'), false)
->assertSee(route('feed.sitemap'), false)
->assertSee(route('feed.llms'), false);
}
/**
* @param array<string, mixed> $meta
*/
private function makePost(string $title, string $slug, string $status, string $content = '内容', array $meta = []): Post
{
return Post::create([
'user_id' => $this->author->id,
'category_id' => $this->category->id,
'title' => $title,
'slug' => $slug,
'content' => $content,
'content_format' => 'markdown',
'status' => $status,
'meta' => $meta,
'published_at' => $status === 'published' ? now() : null,
]);
}
}
+4 -2
View File
@@ -7,7 +7,6 @@ use App\Models\Category;
use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Tests\TestCase;
class PageCacheTest extends TestCase
@@ -44,6 +43,9 @@ class PageCacheTest extends TestCase
// 第二次命中缓存
$this->get('/')->assertOk();
$this->assertSame(1, PageCacheMiddleware::count());
$this->get('/?view=full')->assertOk();
$this->assertSame(2, PageCacheMiddleware::count());
}
public function test_authenticated_user_not_cached(): void
@@ -63,7 +65,7 @@ class PageCacheTest extends TestCase
$post = Post::where('slug', 'cache-test')->first();
$views = $post->views;
$this->get('/posts/cache-test.shtml')->assertOk();
$this->get($post->url)->assertOk();
$this->get('/track-view/'.$post->id)->assertStatus(204);
$post->refresh();
+114 -13
View File
@@ -6,7 +6,11 @@ namespace Tests\Feature;
use App\Filament\Resources\Posts\Pages\EditPost;
use App\Filament\Resources\Posts\PostResource;
use App\Filament\Resources\Posts\Schemas\PostForm;
use App\Models\Post;
use Filament\Forms\Components\Field;
use Filament\Forms\Components\ToggleButtons;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
use Illuminate\Foundation\Testing\RefreshDatabase;
@@ -18,31 +22,51 @@ class PostFormInjectionTest extends TestCase
{
use RefreshDatabase;
public function test_post_form_includes_paid_section_from_membership_plugin(): void
public function test_post_form_uses_two_to_one_layout(): void
{
$schema = Schema::make();
PostResource::form($schema);
// Section 的标题在 heading 而非 label
$headings = collect($schema->getComponents())
->map(fn ($component) => method_exists($component, 'getHeading') ? $component->getHeading() : $component->getLabel())
->all();
$roots = $schema->getComponents();
$this->assertCount(2, $roots);
$this->assertContains('付费设置', $headings);
[$content, $sidebar] = $roots;
$this->assertSame('正文', $content->getHeading());
$this->assertSame(PostForm::SIDEBAR_KEY, $sidebar->getKey(isAbsolute: false));
$this->assertSame(2, $content->getColumnSpan('default'));
$this->assertSame(2, $content->getColumnSpan('lg'));
$this->assertSame(1, $sidebar->getColumnSpan('default'));
$this->assertSame(1, $sidebar->getColumnSpan('lg'));
}
// 找到付费设置 Section;无 Livewire 环境下用反射读取默认内容槽(childComponents 按 schema key 分组)
$section = collect($schema->getComponents())
->first(fn ($component) => $component->getHeading() === '付费设置');
public function test_post_form_includes_paid_fields_in_access_section(): void
{
$schema = Schema::make();
PostResource::form($schema);
$property = (new ReflectionClass($section))->getProperty('childComponents');
$fieldNames = collect($property->getValue($section)['default'] ?? [])
->map(fn ($field) => $field->getName())
->all();
$access = $this->findKeyedComponent($schema->getComponents(), PostForm::ACCESS_KEY);
$this->assertNotNull($access);
$this->assertSame('访问设置', $access->getHeading());
$fieldNames = $this->fieldNames($access);
$this->assertContains('read_password', $fieldNames);
$this->assertContains('meta.price', $fieldNames);
$this->assertContains('meta.members_only', $fieldNames);
}
public function test_content_format_and_status_are_single_choice(): void
{
$schema = Schema::make();
PostResource::form($schema);
$fields = $this->collectFields($schema->getComponents());
$this->assertInstanceOf(ToggleButtons::class, $fields['content_format'] ?? null);
$this->assertInstanceOf(ToggleButtons::class, $fields['status'] ?? null);
$this->assertInstanceOf(ToggleButtons::class, $fields['close_comment'] ?? null);
$this->assertInstanceOf(ToggleButtons::class, $fields['sticky_level'] ?? null);
}
public function test_post_table_includes_paid_badge_column(): void
{
// Table 构造器要求 HasTable Livewire 组件,测试中绕过构造器
@@ -93,4 +117,81 @@ class PostFormInjectionTest extends TestCase
$this->assertSame(9900, $post->meta['price']);
$this->assertTrue($post->meta['members_only']);
}
/**
* @param array<int, mixed> $components
*/
private function findKeyedComponent(array $components, string $key): ?Component
{
foreach ($components as $component) {
if (! $component instanceof Component) {
continue;
}
$property = (new ReflectionClass($component))->getProperty('key');
$componentKey = $property->getValue($component);
if ($componentKey === $key) {
return $component;
}
$found = $this->findKeyedComponent($this->childComponents($component), $key);
if ($found instanceof Component) {
return $found;
}
}
return null;
}
/**
* @return list<string>
*/
private function fieldNames(Component $section): array
{
return collect($this->childComponents($section))
->filter(fn ($field) => is_object($field) && method_exists($field, 'getName'))
->map(fn ($field) => $field->getName())
->values()
->all();
}
/**
* @param array<int, mixed> $components
* @return array<string, Field>
*/
private function collectFields(array $components): array
{
$fields = [];
foreach ($components as $component) {
if ($component instanceof Field) {
$fields[$component->getName()] = $component;
}
if ($component instanceof Component) {
$fields = [
...$fields,
...$this->collectFields($this->childComponents($component)),
];
}
}
return $fields;
}
/**
* @return array<int, mixed>
*/
private function childComponents(?Component $component): array
{
if (! $component instanceof Component) {
return [];
}
$property = (new ReflectionClass($component))->getProperty('childComponents');
$children = $property->getValue($component)['default'] ?? [];
return is_array($children) ? $children : [];
}
}
+24
View File
@@ -158,6 +158,30 @@ class SablogImportTest extends TestCase
$this->assertSame('photo.jpg', $media->file_name);
}
public function test_import_normalizes_highlighter_before_markdown(): void
{
$pdo = new PDO('sqlite:'.$this->legacyDb);
$html = '<p>&nbsp;不多说,上代码:</p>'
.'<div class="codeText"><div class="codeHead">XML/HTML代码</div>'
.'<ol class="dp-xml"><li>docker&nbsp;run -d\\</li><li>&nbsp; --name derper</li></ol></div>';
$statement = $pdo->prepare('INSERT INTO sablog_articles VALUES (12, 2, 1, ?, ?, ?, ?, ?, 1, 0, ?, 0, 0, 0, 1, 0, ?)');
$statement->execute(['还是得derp', '', $html, '', time(), '', '']);
$this->import(['--convert-markdown' => true]);
$post = Post::find(12);
$this->assertNotNull($post);
$this->assertSame('markdown', $post->content_format);
$this->assertStringNotContainsString('&nbsp;', $post->content);
$this->assertStringNotContainsString('codeText', $post->content);
$this->get('/show-12.shtml')
->assertOk()
->assertDontSee('&nbsp;', false)
->assertSee('docker run', false)
->assertSee('<pre', false);
}
public function test_import_is_idempotent(): void
{
$this->import(['--sync-attachments' => true]);
+123
View File
@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace Tests\Feature;
use App\Blog\Support\PageCacheMiddleware;
use App\Models\Category;
use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class StickyPreviewTest extends TestCase
{
use RefreshDatabase;
private User $admin;
private Category $category;
protected function setUp(): void
{
parent::setUp();
$this->seed();
$this->admin = User::query()->where('email', 'admin@laralog.test')->firstOrFail();
$this->category = Category::query()->first() ?? Category::create([
'name' => '测试分类',
'slug' => 'sticky-test-cat',
]);
}
public function test_sticky_headline_stays_on_top(): void
{
$headline = $this->makePost('头条文章', Post::STICKY_HEADLINE, now()->subDays(3));
$strong = $this->makePost('强置顶文章', Post::STICKY_STRONG, now()->subDay());
$normal = $this->makePost('普通文章', Post::STICKY_NONE, now());
$ids = Post::query()->published()->orderedForList()->pluck('id')->all();
$this->assertSame(
[$headline->id, $strong->id, $normal->id],
array_values(array_intersect($ids, [$headline->id, $strong->id, $normal->id]))
);
$headline->refresh();
$this->assertTrue($headline->is_sticky);
$this->assertSame(Post::STICKY_HEADLINE, $headline->sticky_level);
$this->assertSame('置顶头条', $headline->stickyLabel());
}
public function test_guest_cannot_preview_draft_but_editor_can(): void
{
$draft = $this->makePost('草稿预览', Post::STICKY_NONE, null, 'draft');
$this->get($draft->url)->assertNotFound();
$this->actingAs($this->admin)
->get($draft->url)
->assertOk()
->assertSee('草稿预览', false)
->assertSee('预览中', false);
$editor = User::factory()->create([
'email' => 'editor-preview@laralog.test',
]);
$editor->assignRole('editor');
$this->actingAs($editor)
->get($draft->url)
->assertOk()
->assertSee('预览中', false);
$member = User::factory()->create([
'email' => 'member-preview@laralog.test',
]);
$member->assignRole('member');
$this->actingAs($member)
->get($draft->url)
->assertNotFound();
}
public function test_edit_page_has_preview_action(): void
{
$post = $this->makePost('可预览文章', Post::STICKY_LIGHT, now());
$this->actingAs($this->admin)
->get('/admin/posts/'.$post->id.'/edit')
->assertOk()
->assertSee('预览', false)
->assertSee($post->url, false);
}
public function test_page_cache_entries_store_url(): void
{
PageCacheMiddleware::flush();
$this->get('/')->assertOk();
$entries = PageCacheMiddleware::entries();
$this->assertNotEmpty($entries);
$this->assertNotSame('', $entries[0]['url']);
$this->assertSame(1, PageCacheMiddleware::count());
}
private function makePost(string $title, int $level, mixed $publishedAt, string $status = 'published'): Post
{
return Post::create([
'user_id' => $this->admin->id,
'category_id' => $this->category->id,
'title' => $title,
'slug' => str()->slug($title).'-'.uniqid(),
'content' => '内容',
'content_format' => 'markdown',
'status' => $status,
'sticky_level' => $level,
'published_at' => $publishedAt,
]);
}
}
@@ -1,6 +1,7 @@
<footer class="d-footer">
<div class="d-container">
<p>© {{ date('Y') }} {{ $siteName }}@if($siteIcp) · <a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">{{ $siteIcp }}</a>@endif</p>
<p>@include('partials.feed-links')</p>
<p>由 LaraLog 驱动 · Demo 主题</p>
</div>
</footer>
@@ -11,6 +11,7 @@
<meta name="generator" content="LaraLog">
<link rel="canonical" href="{{ url()->current() }}">
<link rel="alternate" type="application/rss+xml" title="{{ $siteName }}" href="{{ route('feed.rss') }}">
<link rel="alternate" type="text/plain" title="llms.txt" href="{{ route('feed.llms') }}">
{{-- 主题资产:/themes/demo/assets/style.css --}}
<link rel="stylesheet" href="{{ theme('asset', 'style.css') }}">
@stack('head')
@@ -6,6 +6,7 @@
<a href="{{ route('archives') }}" class="{{ request()->routeIs('archives*') ? 'active' : '' }}">归档</a>
<a href="{{ route('tags') }}" class="{{ request()->routeIs('tags*') ? 'active' : '' }}">标签</a>
<a href="{{ route('links') }}" class="{{ request()->routeIs('links') ? 'active' : '' }}">友链</a>
@include('partials.feed-links')
<form class="d-search" action="{{ route('search') }}" method="get">
<input type="search" name="q" placeholder="搜索..." value="{{ request('q', request('keyword', '')) }}">
</form>
+732 -138
View File
@@ -17,182 +17,776 @@
--border: #2c2c2c;
}
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; font-size: 16px; line-height: 1.8; }
a { color: var(--accent); text-decoration: none; }
a:hover { opacity: 0.8; }
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: var(--bg);
color: var(--text);
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
"Microsoft YaHei", sans-serif;
font-size: 16px;
line-height: 1.8;
}
a {
color: var(--accent);
text-decoration: none;
}
a:hover {
opacity: 0.8;
}
/* 页面统一宽度:页头/页脚与主体对齐(默认视图的 .container 也落在这里) */
.container, .m-container { max-width: 1080px; margin: 0 auto; padding: 0 20px; }
.container,
.m-container {
max-width: 1080px;
margin: 0 auto;
padding: 0 20px;
}
.m-header { border-bottom: 1px solid var(--border); background: var(--card); }
.m-header .m-container { display: flex; justify-content: space-between; align-items: center; padding: 18px 20px; }
.m-logo { font-size: 22px; font-weight: 800; letter-spacing: 1px; color: var(--text); }
.m-nav { display: flex; gap: 18px; font-size: 14px; align-items: center; }
.m-nav a.active { font-weight: 700; }
.m-link-btn { background: none; border: none; color: var(--accent); font-size: 14px; cursor: pointer; padding: 0; }
.m-search input { padding: 7px 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg); color: var(--text); font-size: 14px; width: 140px; }
.inline-form { display: inline; }
.m-header {
border-bottom: 1px solid var(--border);
background: var(--card);
}
.m-header .m-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 18px 20px;
}
.m-logo {
font-size: 22px;
font-weight: 800;
letter-spacing: 1px;
color: var(--text);
}
.m-nav {
display: flex;
gap: 18px;
font-size: 14px;
align-items: center;
}
.m-nav a.active {
font-weight: 700;
}
.m-link-btn {
background: none;
border: none;
color: var(--accent);
font-size: 14px;
cursor: pointer;
padding: 0;
}
.m-search input {
padding: 7px 12px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg);
color: var(--text);
font-size: 14px;
width: 140px;
}
.inline-form {
display: inline;
}
.main-layout { display: flex; gap: 32px; align-items: flex-start; max-width: 1080px; margin: 0 auto; padding: 36px 20px; }
.main-layout .content { flex: 1; min-width: 0; max-width: 720px; }
.main-layout .sidebar { width: 300px; flex-shrink: 0; }
.main-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 300px;
gap: 32px;
align-items: start;
max-width: 1080px;
margin: 0 auto;
padding: 36px 20px;
}
.main-layout .content {
min-width: 0;
max-width: 100%;
}
.main-layout .sidebar {
width: auto;
min-width: 0;
}
.post-card, .post-full, .comments-section { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 26px 30px; margin-bottom: 22px; }
.post-card.sticky { box-shadow: inset 3px 0 0 var(--accent); }
.post-title { font-size: 20px; margin-bottom: 8px; line-height: 1.45; }
.post-card .post-title a { color: var(--text); }
.badge { background: var(--accent); color: #fff; font-size: 11px; padding: 2px 7px; border-radius: 3px; margin-right: 6px; vertical-align: 2px; }
.post-meta { color: var(--muted); font-size: 13px; margin-bottom: 12px; }
.post-excerpt { color: var(--muted); }
.post-footer { margin-top: 12px; display: flex; gap: 12px; flex-wrap: wrap; font-size: 14px; }
.read-more { font-weight: 600; }
.post-card,
.post-full,
.comments-section {
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 26px 30px;
margin-bottom: 22px;
}
.post-card.sticky {
box-shadow: inset 3px 0 0 var(--accent);
}
.post-title {
font-size: 20px;
margin-bottom: 8px;
line-height: 1.45;
}
.post-card .post-title a {
color: var(--text);
}
.badge {
background: var(--accent);
color: #fff;
font-size: 11px;
padding: 2px 7px;
border-radius: 3px;
margin-right: 6px;
vertical-align: 2px;
}
.post-meta {
color: var(--muted);
font-size: 13px;
margin-bottom: 12px;
}
.post-excerpt {
color: var(--muted);
}
.post-footer {
margin-top: 12px;
display: flex;
gap: 12px;
flex-wrap: wrap;
font-size: 14px;
}
.read-more {
font-weight: 600;
}
.post-full .post-title { font-size: 28px; }
.post-content { margin-top: 14px; }
.post-content img { max-width: 100%; border-radius: 8px; }
.post-content pre { background: #0d1117; color: #c9d1d9; padding: 16px; border-radius: 8px; overflow-x: auto; margin: 14px 0; }
.post-content code { background: rgba(128,128,128,.15); padding: 2px 6px; border-radius: 4px; font-size: 14px; }
.post-content pre code { background: none; padding: 0; }
.post-content blockquote { border-left: 3px solid var(--accent); padding-left: 16px; color: var(--muted); margin: 14px 0; }
.post-content h1, .post-content h2, .post-content h3 { margin: 20px 0 8px; }
.post-full .post-title {
font-size: 28px;
}
.post-content {
margin-top: 14px;
max-width: 100%;
overflow-wrap: anywhere;
word-break: break-word;
}
.post-content img {
max-width: 100%;
height: auto;
border-radius: 8px;
}
.post-content pre,
.post-content .codeText,
.post-content ol[class^="dp-"] {
background: #0d1117;
color: #c9d1d9;
padding: 16px;
border-radius: 8px;
overflow-x: auto;
max-width: 100%;
margin: 14px 0;
white-space: pre-wrap;
word-break: break-word;
}
.post-content code {
background: rgba(128, 128, 128, 0.15);
padding: 2px 6px;
border-radius: 4px;
font-size: 14px;
word-break: break-all;
overflow-wrap: anywhere;
white-space: break-spaces;
}
.post-content pre code {
display: block;
background: none;
padding: 0;
white-space: pre-wrap;
word-break: break-word;
}
.post-content blockquote {
border-left: 3px solid var(--accent);
padding-left: 16px;
color: var(--muted);
margin: 14px 0;
}
.post-content h1,
.post-content h2,
.post-content h3 {
margin: 20px 0 8px;
}
.post-cover { margin: -26px -30px 18px; border-radius: 12px 12px 0 0; overflow: hidden; }
.post-cover img { width: 100%; display: block; max-height: 380px; object-fit: cover; }
.post-cover {
margin: -26px -30px 18px;
border-radius: 12px 12px 0 0;
overflow: hidden;
}
.post-cover img {
width: 100%;
display: block;
max-height: 380px;
object-fit: cover;
}
.tag-link { color: var(--accent); font-size: 14px; margin-right: 8px; }
.tag-link {
color: var(--accent);
font-size: 14px;
margin-right: 8px;
}
.comments-section { margin-top: 8px; }
.section-title { font-size: 17px; margin-bottom: 14px; }
.comment { padding: 12px 0; border-bottom: 1px solid var(--border); }
.comment:last-of-type { border-bottom: none; }
.comment-head { font-size: 14px; margin-bottom: 4px; }
.comment-time { color: var(--muted); font-size: 13px; }
.comment-body { color: var(--text); font-size: 15px; }
.comment-form { margin-top: 14px; }
.form-row { margin-bottom: 12px; }
.form-row label { display: block; font-size: 14px; margin-bottom: 4px; color: var(--muted); }
.form-row input, .form-row textarea { width: 100%; padding: 10px 14px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg); color: var(--text); font-size: 15px; font-family: inherit; }
.honeypot { position: absolute; left: -9999px; opacity: 0; }
.error { color: var(--accent); font-size: 13px; }
.btn { background: var(--accent); color: #fff; border: none; padding: 10px 24px; border-radius: 8px; font-size: 15px; cursor: pointer; }
.alert { padding: 12px 18px; border-radius: 8px; margin-bottom: 16px; font-size: 14px; }
.alert-success { background: rgba(46,160,67,.12); color: #2ea043; }
.alert-error { background: rgba(248,81,73,.12); color: #f85149; }
.empty { color: var(--muted); text-align: center; padding: 40px 0; }
.comments-section {
margin-top: 8px;
}
.section-title {
font-size: 17px;
margin-bottom: 14px;
}
.comment {
padding: 12px 0;
border-bottom: 1px solid var(--border);
}
.comment:last-of-type {
border-bottom: none;
}
.comment-head {
font-size: 14px;
margin-bottom: 4px;
}
.comment-time {
color: var(--muted);
font-size: 13px;
}
.comment-body {
color: var(--text);
font-size: 15px;
}
.comment-form {
margin-top: 14px;
}
.form-row {
margin-bottom: 12px;
}
.form-row label {
display: block;
font-size: 14px;
margin-bottom: 4px;
color: var(--muted);
}
.form-row input,
.form-row textarea {
width: 100%;
padding: 10px 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg);
color: var(--text);
font-size: 15px;
font-family: inherit;
}
.honeypot {
position: absolute;
left: -9999px;
opacity: 0;
}
.error {
color: var(--accent);
font-size: 13px;
}
.btn {
background: var(--accent);
color: #fff;
border: none;
padding: 10px 24px;
border-radius: 8px;
font-size: 15px;
cursor: pointer;
}
.alert {
padding: 12px 18px;
border-radius: 8px;
margin-bottom: 16px;
font-size: 14px;
}
.alert-success {
background: rgba(46, 160, 67, 0.12);
color: #2ea043;
}
.alert-error {
background: rgba(248, 81, 73, 0.12);
color: #f85149;
}
.alert-warning {
background: rgba(210, 153, 34, 0.12);
color: #9a6700;
}
.empty {
color: var(--muted);
text-align: center;
padding: 40px 0;
}
.list-title { font-size: 24px; margin-bottom: 20px; }
.list-title {
font-size: 24px;
margin-bottom: 20px;
}
.archive-year { font-size: 16px; margin: 16px 0 6px; }
.archive-list { list-style: none; }
.archive-posts { margin: 4px 0 12px 22px; }
.archive-posts time { color: var(--muted); font-size: 13px; }
.archive-year {
font-size: 16px;
margin: 16px 0 6px;
}
.archive-list {
list-style: none;
}
.archive-posts {
margin: 4px 0 12px 22px;
}
.archive-posts time {
color: var(--muted);
font-size: 13px;
}
.pagination { margin: 16px 0; }
.pagination-links { display: flex; gap: 8px; flex-wrap: wrap; }
.pagination-links a, .pagination-links .current, .pagination-links .disabled { padding: 6px 14px; border: 1px solid var(--border); border-radius: 6px; font-size: 14px; background: var(--card); color: var(--text); }
.pagination-links .current { background: var(--accent); color: #fff; border-color: var(--accent); }
.pagination-links .disabled { color: var(--muted); }
.view-mode-switcher {
font-size: 13px;
color: var(--muted);
margin-bottom: 16px;
}
.view-mode-switcher a.active {
font-weight: 700;
color: var(--text);
}
.m-footer { border-top: 1px solid var(--border); padding: 26px 0 40px; margin-top: 30px; text-align: center; color: var(--muted); font-size: 13px; }
.pagination {
width: 100%;
max-width: 100%;
min-width: 0;
margin: 16px 0;
}
.pagination-links {
display: flex;
gap: 6px;
flex-wrap: wrap;
align-items: center;
max-width: 100%;
}
.pagination-info {
color: var(--muted);
font-size: 13px;
margin-right: 6px;
white-space: nowrap;
}
.pagination-links a,
.pagination-links .current,
.pagination-links .disabled {
padding: 6px 14px;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 14px;
background: var(--card);
color: var(--text);
}
.pagination-links .current {
background: var(--accent);
color: #fff;
border-color: var(--accent);
}
.pagination-links .disabled {
color: var(--muted);
}
.tag-cloud.big { line-height: 2.4; }
.links-list { list-style: none; }
.links-list li { padding: 6px 0; }
.link-note { color: var(--muted); font-size: 14px; }
.m-footer {
border-top: 1px solid var(--border);
padding: 26px 0 40px;
margin-top: 30px;
text-align: center;
color: var(--muted);
font-size: 13px;
}
.auth-layout { display: flex; justify-content: center; padding: 50px 20px; }
.auth-box { width: 100%; max-width: 420px; background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 30px; }
.auth-alt { margin-top: 14px; font-size: 14px; color: var(--muted); text-align: center; }
.search-form { margin-bottom: 16px; display: flex; gap: 8px; }
.search-form input { flex: 1; padding: 10px 14px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg); color: var(--text); font-size: 15px; }
.tag-cloud.big {
line-height: 2.4;
}
.links-list {
list-style: none;
}
.links-list li {
padding: 6px 0;
}
.link-note {
color: var(--muted);
font-size: 14px;
}
.auth-layout {
display: flex;
justify-content: center;
padding: 50px 20px;
}
.auth-box {
width: 100%;
max-width: 420px;
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 30px;
}
.auth-alt {
margin-top: 14px;
font-size: 14px;
color: var(--muted);
text-align: center;
}
.search-form {
margin-bottom: 16px;
display: flex;
gap: 8px;
}
.search-form input {
flex: 1;
padding: 10px 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg);
color: var(--text);
font-size: 15px;
}
/* ============ 侧边栏 ============ */
.sidebar { position: sticky; top: 20px; }
.widget { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 18px 20px; margin-bottom: 18px; }
.widget-title { font-size: 15px; margin-bottom: 10px; padding-bottom: 8px; border-bottom: 2px solid var(--accent); }
.widget-list { list-style: none; }
.widget-list li { font-size: 14px; padding: 5px 0; border-bottom: 1px solid var(--border); }
.sidebar {
position: sticky;
top: 20px;
}
.widget {
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 18px 20px;
margin-bottom: 18px;
}
.widget-title {
font-size: 15px;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 2px solid var(--accent);
}
.widget-list {
list-style: none;
}
.widget-list li {
font-size: 14px;
padding: 5px 0;
border-bottom: 1px solid var(--border);
}
/* 社交账号:横向胶囊列表 */
.social-links { list-style: none; display: flex; flex-wrap: wrap; gap: 8px; }
.social-links li { font-size: 12px; background: var(--bg); border: 1px solid var(--border); border-radius: 999px; padding: 4px 10px; }
.widget-list li:last-child { border-bottom: none; }
.count { color: var(--muted); font-size: 13px; }
.tag-cloud { line-height: 2; }
.tag-cloud a { display: inline-block; }
.tag-cloud.big { line-height: 2.4; }
.social-links {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.social-links li {
font-size: 12px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 999px;
padding: 4px 10px;
}
.widget-list li:last-child {
border-bottom: none;
}
.count {
color: var(--muted);
font-size: 13px;
}
.tag-cloud {
line-height: 2;
}
.tag-cloud a {
display: inline-block;
}
.tag-cloud.big {
line-height: 2.4;
}
/* ============ 顶栏搜索(默认 header partial) ============ */
.site-search input { padding: 7px 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg); color: var(--text); font-size: 14px; width: 160px; }
.link-btn { background: none; border: none; color: var(--accent); font-size: 15px; cursor: pointer; padding: 0; }
.site-search input {
padding: 7px 12px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg);
color: var(--text);
font-size: 14px;
width: 160px;
}
.link-btn {
background: none;
border: none;
color: var(--accent);
font-size: 15px;
cursor: pointer;
padding: 0;
}
/* ============ 文章正文补充 ============ */
.post-tags { margin-top: 16px; }
.post-content ul, .post-content ol { padding-left: 24px; margin: 8px 0; }
.post-content h4, .post-content h5, .post-content h6 { margin: 16px 0 6px; }
.post-content table { border-collapse: collapse; width: 100%; margin: 12px 0; }
.post-content th, .post-content td { border: 1px solid var(--border); padding: 6px 10px; }
.post-content hr { border: none; border-top: 1px solid var(--border); margin: 18px 0; }
.post-tags {
margin-top: 16px;
}
.post-content ul,
.post-content ol {
padding-left: 24px;
margin: 8px 0;
}
.post-content h4,
.post-content h5,
.post-content h6 {
margin: 16px 0 6px;
}
.post-content table {
border-collapse: collapse;
width: 100%;
margin: 12px 0;
}
.post-content th,
.post-content td {
border: 1px solid var(--border);
padding: 6px 10px;
}
.post-content hr {
border: none;
border-top: 1px solid var(--border);
margin: 18px 0;
}
/* ============ 个人中心 ============ */
.profile-box { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 24px 28px; margin-bottom: 20px; }
.profile-box p { margin-bottom: 8px; }
.profile-box {
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 24px 28px;
margin-bottom: 20px;
}
.profile-box p {
margin-bottom: 8px;
}
/* ============ 按钮变体 ============ */
.btn-primary { background: var(--accent); }
.btn:hover { opacity: 0.9; }
.btn-primary {
background: var(--accent);
}
.btn:hover {
opacity: 0.9;
}
/* ============ 会员/支付 ============ */
.plans-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 16px; }
.plan-card .plan-price { font-size: 26px; font-weight: 800; color: var(--accent); }
.paid-teaser { background: rgba(248,81,73,.08); border: 1px dashed var(--accent); border-radius: 8px; padding: 12px 16px; color: var(--muted); font-size: 14px; margin: 12px 0; }
.plans-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 16px;
}
.plan-card .plan-price {
font-size: 26px;
font-weight: 800;
color: var(--accent);
}
.paid-teaser {
background: rgba(248, 81, 73, 0.08);
border: 1px dashed var(--accent);
border-radius: 8px;
padding: 12px 16px;
color: var(--muted);
font-size: 14px;
margin: 12px 0;
}
/* ============ 移动端适配 ============ */
@media (max-width: 768px) {
.m-header .m-container { flex-direction: column; align-items: flex-start; gap: 12px; padding: 16px 20px; }
.m-nav { flex-wrap: wrap; gap: 12px; width: 100%; }
.m-search { flex: 1; min-width: 100%; }
.m-search input { width: 100%; }
.main-layout { flex-direction: column; padding: 24px 16px; }
.main-layout .content { max-width: 100%; }
.main-layout .sidebar { width: 100%; }
.post-card, .post-full, .comments-section { padding: 20px; }
.post-cover { margin: -20px -20px 16px; }
.post-full .post-title { font-size: 24px; }
.auth-box { padding: 24px 20px; }
.pagination-links { gap: 6px; }
.pagination-links a, .pagination-links .current, .pagination-links .disabled { padding: 5px 10px; }
.m-header .m-container {
flex-direction: column;
align-items: flex-start;
gap: 12px;
padding: 16px 20px;
}
.m-nav {
flex-wrap: wrap;
gap: 12px;
width: 100%;
}
.m-search {
flex: 1;
min-width: 100%;
}
.m-search input {
width: 100%;
}
.main-layout {
grid-template-columns: minmax(0, 1fr);
padding: 24px 16px;
}
.main-layout .content {
max-width: 100%;
}
.main-layout .sidebar {
width: 100%;
}
.post-card,
.post-full,
.comments-section {
padding: 20px;
}
.post-cover {
margin: -20px -20px 16px;
}
.post-full .post-title {
font-size: 24px;
}
.auth-box {
padding: 24px 20px;
}
.pagination-links {
gap: 6px;
}
.pagination-links a,
.pagination-links .current,
.pagination-links .disabled {
padding: 5px 10px;
}
}
/* TOC */
.toc { background: var(--bg); border: 1px solid var(--border); border-radius: 10px; padding: 14px 18px; margin: 0 0 18px; font-size: 14px; }
.toc summary { cursor: pointer; font-weight: 600; margin-bottom: 6px; }
.toc-list { list-style: none; }
.toc-list li { padding: 3px 0; }
.toc-list a { color: var(--muted); }
.toc-list a:hover { color: var(--accent); }
.toc-level-3 { padding-left: 14px; }
.toc-level-4, .toc-level-5, .toc-level-6 { padding-left: 28px; }
.toc {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 10px;
padding: 14px 18px;
margin: 0 0 18px;
font-size: 14px;
}
.toc summary {
cursor: pointer;
font-weight: 600;
margin-bottom: 6px;
}
.toc-list {
list-style: none;
}
.toc-list li {
padding: 3px 0;
}
.toc-list a {
color: var(--muted);
}
.toc-list a:hover {
color: var(--accent);
}
.toc-level-3 {
padding-left: 14px;
}
.toc-level-4,
.toc-level-5,
.toc-level-6 {
padding-left: 28px;
}
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
/* Paywall */
.paywall { border: 1px solid var(--border); border-radius: 12px; padding: 28px 22px; background: var(--bg); text-align: center; }
.paywall-cover img { max-width: 100%; border-radius: 10px; margin-bottom: 16px; }
.paywall-title { font-size: 22px; margin: 0 0 10px; }
.paywall-excerpt { color: var(--muted); font-size: 14px; margin: 0 0 16px; }
.paywall-price { font-size: 24px; font-weight: 800; color: var(--accent); margin: 0 0 8px; }
.paywall-hint { color: var(--muted); font-size: 13px; margin: 0 0 18px; }
.paywall-actions .btn { margin: 0 6px; }
.paid-teaser { border: 1px dashed var(--border); background: var(--bg-muted, #f6f6f6); color: var(--muted); border-radius: 10px; padding: 14px 16px; margin: 14px 0; font-size: 14px; }
.paid-teaser .btn { margin-left: 8px; }
.paywall {
border: 1px solid var(--border);
border-radius: 12px;
padding: 28px 22px;
background: var(--bg);
text-align: center;
}
.paywall-cover img {
max-width: 100%;
border-radius: 10px;
margin-bottom: 16px;
}
.paywall-title {
font-size: 22px;
margin: 0 0 10px;
}
.paywall-excerpt {
color: var(--muted);
font-size: 14px;
margin: 0 0 16px;
}
.paywall-price {
font-size: 24px;
font-weight: 800;
color: var(--accent);
margin: 0 0 8px;
}
.paywall-hint {
color: var(--muted);
font-size: 13px;
margin: 0 0 18px;
}
.paywall-actions .btn {
margin: 0 6px;
}
.paid-teaser {
border: 1px dashed var(--border);
background: var(--bg-muted, #f6f6f6);
color: var(--muted);
border-radius: 10px;
padding: 14px 16px;
margin: 14px 0;
font-size: 14px;
}
.paid-teaser .btn {
margin-left: 8px;
}
/* 已解锁的付费内容块 */
.paid-content { border: 1px solid var(--border); border-left: 4px solid var(--accent); background: var(--bg); border-radius: 10px; padding: 14px 16px; margin: 14px 0; }
.paid-content-label { font-size: 12px; color: var(--accent); font-weight: 600; margin-bottom: 6px; }
.paid-content {
border: 1px solid var(--border);
border-left: 4px solid var(--accent);
background: var(--bg);
border-radius: 10px;
padding: 14px 16px;
margin: 14px 0;
}
.paid-content-label {
font-size: 12px;
color: var(--accent);
font-weight: 600;
margin-bottom: 6px;
}
/* 分类页介绍区 */
.category-intro { display: flex; gap: 14px; align-items: flex-start; background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 16px 18px; margin-bottom: 18px; }
.category-cover { width: 128px; height: 84px; object-fit: cover; border-radius: 8px; flex-shrink: 0; }
.category-description { margin: 0 0 6px; font-size: 14px; color: var(--muted); line-height: 1.7; }
.category-meta { margin: 0; font-size: 12px; color: var(--muted); }
.category-intro {
display: flex;
gap: 14px;
align-items: flex-start;
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px 18px;
margin-bottom: 18px;
}
.category-cover {
width: 128px;
height: 84px;
object-fit: cover;
border-radius: 8px;
flex-shrink: 0;
}
.category-description {
margin: 0 0 6px;
font-size: 14px;
color: var(--muted);
line-height: 1.7;
}
.category-meta {
margin: 0;
font-size: 12px;
color: var(--muted);
}
@@ -4,6 +4,7 @@
<p>&copy; {{ date('Y') }} {{ $siteName }}
@if($siteIcp)<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">{{ $siteIcp }}</a>@endif
</p>
<p>@include('partials.feed-links')</p>
<p>LaraLog · 用 Markdown 书写,用 AI 审核</p>
</div>
</footer>
@@ -12,6 +12,7 @@
<meta name="generator" content="LaraLog">
<link rel="canonical" href="{{ url()->current() }}">
<link rel="alternate" type="application/rss+xml" title="{{ $siteName }}" href="{{ route('feed.rss') }}">
<link rel="alternate" type="text/plain" title="llms.txt" href="{{ route('feed.llms') }}">
<link rel="stylesheet" href="{{ theme('asset', 'style.css') }}">
@stack('head')
@stack('theme:head')
@@ -6,6 +6,7 @@
<a href="{{ route('archives') }}" class="{{ request()->routeIs('archives*') ? 'active' : '' }}">归档</a>
<a href="{{ route('tags') }}" class="{{ request()->routeIs('tags*') ? 'active' : '' }}">标签</a>
<a href="{{ route('links') }}" class="{{ request()->routeIs('links') ? 'active' : '' }}">友链</a>
@include('partials.feed-links')
<form class="m-search" action="{{ route('search') }}" method="get">
<input type="search" name="q" placeholder="搜索..." value="{{ request('q', request('keyword', '')) }}">
</form>
+640 -124
View File
@@ -1,175 +1,691 @@
/* ============ Sablog 经典主题 ============ */
* { margin: 0; padding: 0; box-sizing: border-box; }
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #eef2f5 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==) repeat-x top;
background: #eef2f5
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==)
repeat-x top;
font-family: Verdana, "PingFang SC", "Microsoft YaHei", sans-serif;
font-size: 13px;
color: #333;
line-height: 1.7;
}
a { color: #1c62b0; text-decoration: none; }
a:hover { color: #f60; text-decoration: underline; }
a {
color: #1c62b0;
text-decoration: none;
}
a:hover {
color: #f60;
text-decoration: underline;
}
#outmain { width: 960px; margin: 0 auto; }
#outmain {
width: 960px;
max-width: 100%;
margin: 0 auto;
}
#header { padding: 22px 10px 8px; }
.logo a { font-size: 30px; font-weight: bold; color: #1c62b0; font-family: Georgia, serif; }
.logo .description { color: #888; font-size: 12px; }
#header {
padding: 22px 10px 8px;
}
.logo a {
font-size: 30px;
font-weight: bold;
color: #1c62b0;
font-family: Georgia, serif;
}
.logo .description {
color: #888;
font-size: 12px;
}
#nav { margin-top: 14px; border-top: 1px solid #d8dfe6; border-bottom: 1px solid #d8dfe6; background: #f7f9fb; }
#nav ul { list-style: none; display: flex; flex-wrap: wrap; }
#nav li { border-right: 1px solid #d8dfe6; }
#nav a { display: block; padding: 7px 18px; color: #333; font-weight: bold; font-size: 13px; }
#nav a:hover { background: #1c62b0; color: #fff; text-decoration: none; }
#nav .current_page_item a { background: #1c62b0; color: #fff; }
#nav {
margin-top: 14px;
border-top: 1px solid #d8dfe6;
border-bottom: 1px solid #d8dfe6;
background: #f7f9fb;
}
#nav ul {
list-style: none;
display: flex;
flex-wrap: wrap;
}
#nav li {
border-right: 1px solid #d8dfe6;
}
#nav a {
display: block;
padding: 7px 18px;
color: #333;
font-weight: bold;
font-size: 13px;
}
#nav a:hover {
background: #1c62b0;
color: #fff;
text-decoration: none;
}
#nav .current_page_item a {
background: #1c62b0;
color: #fff;
}
#page { padding: 14px 10px; display: flex; gap: 18px; align-items: flex-start; }
#page {
padding: 14px 10px;
display: grid;
grid-template-columns: minmax(0, 1fr) 280px;
gap: 18px;
align-items: start;
width: 100%;
max-width: 100%;
}
#content { flex: 1; min-width: 0; }
#content {
min-width: 0;
max-width: 100%;
overflow-wrap: anywhere;
}
.post-card, .post-full, .comments-section, .auth-box, .profile-box, .links-list {
.post-card,
.post-full,
.comments-section,
.auth-box,
.profile-box,
.links-list {
background: #fff;
border: 1px solid #d8dfe6;
padding: 18px 20px;
margin-bottom: 16px;
}
.post-card.sticky { border-left: 4px solid #f60; }
.post-title { font-size: 17px; margin-bottom: 6px; line-height: 1.4; }
.post-card .post-title a { color: #1c62b0; }
.badge { background: #f60; color: #fff; font-size: 11px; padding: 1px 5px; margin-right: 5px; }
.post-meta { color: #999; font-size: 12px; margin-bottom: 8px; }
.post-excerpt { color: #555; margin-bottom: 8px; }
.post-footer { font-size: 12px; }
.tag-link { color: #1c62b0; margin-right: 8px; }
.post-card.sticky {
border-left: 4px solid #f60;
}
.post-title {
font-size: 17px;
margin-bottom: 6px;
line-height: 1.4;
}
.post-card .post-title a {
color: #1c62b0;
}
.badge {
background: #f60;
color: #fff;
font-size: 11px;
padding: 1px 5px;
margin-right: 5px;
}
.post-meta {
color: #999;
font-size: 12px;
margin-bottom: 8px;
}
.post-excerpt {
color: #555;
margin-bottom: 8px;
}
.post-footer {
font-size: 12px;
}
.tag-link {
color: #1c62b0;
margin-right: 8px;
}
.post-full .post-title { font-size: 22px; color: #1c62b0; }
.post-content { margin-top: 10px; font-size: 14px; }
.post-content img { max-width: 100%; }
.post-content pre { background: #f4f4f4; border: 1px solid #ddd; padding: 10px; overflow: auto; margin: 10px 0; }
.post-content blockquote { border-left: 4px solid #1c62b0; padding-left: 12px; color: #666; margin: 10px 0; }
.post-content h1, .post-content h2, .post-content h3 { margin: 14px 0 6px; }
.post-full .post-title {
font-size: 22px;
color: #1c62b0;
}
.post-content {
margin-top: 10px;
font-size: 14px;
max-width: 100%;
overflow-wrap: anywhere;
word-break: break-word;
}
.post-content img {
max-width: 100%;
height: auto;
}
.post-content pre,
.post-content .codeText,
.post-content ol[class^="dp-"] {
background: #f4f4f4;
border: 1px solid #ddd;
border-left: 3px solid #b3cbe1;
padding: 10px;
overflow-x: auto;
max-width: 100%;
margin: 10px 0;
white-space: pre-wrap;
word-break: break-word;
}
.post-content code {
font-family: "Courier New", Courier, monospace;
word-break: break-all;
overflow-wrap: anywhere;
white-space: break-spaces;
}
.post-content pre code {
display: block;
background: none;
padding: 0;
white-space: pre-wrap;
word-break: break-word;
}
.post-content .codeText li {
white-space: pre-wrap;
word-break: break-word;
}
.post-content blockquote {
border-left: 4px solid #1c62b0;
padding-left: 12px;
color: #666;
margin: 10px 0;
}
.post-content h1,
.post-content h2,
.post-content h3 {
margin: 14px 0 6px;
}
.post-cover { margin: -18px -20px 12px; }
.post-cover img { width: 100%; display: block; max-height: 320px; object-fit: cover; }
.post-cover {
margin: -18px -20px 12px;
}
.post-cover img {
width: 100%;
display: block;
max-height: 320px;
object-fit: cover;
}
.comment { padding: 10px 0; border-bottom: 1px dashed #ddd; }
.comment-head { font-size: 12px; margin-bottom: 3px; }
.comment-time { color: #999; }
.comment-body { font-size: 13px; }
.comment-form { margin-top: 12px; }
.form-row { margin-bottom: 8px; }
.form-row label { display: block; font-size: 12px; margin-bottom: 3px; }
.form-row input, .form-row textarea { width: 100%; padding: 6px 8px; border: 1px solid #ccc; font-size: 13px; font-family: inherit; }
.honeypot { position: absolute; left: -9999px; opacity: 0; }
.btn { background: #1c62b0; color: #fff; border: none; padding: 7px 18px; font-size: 13px; cursor: pointer; }
.btn:hover { background: #f60; }
.error { color: #c00; font-size: 12px; }
.alert { padding: 8px 12px; margin-bottom: 12px; border: 1px solid; }
.alert-success { background: #e8f5e9; border-color: #a5d6a7; color: #2e7d32; }
.alert-error { background: #fdecea; border-color: #f5c6cb; color: #c62828; }
.empty { color: #999; text-align: center; padding: 20px 0; }
.list-title { font-size: 18px; margin-bottom: 12px; color: #1c62b0; }
.comment {
padding: 10px 0;
border-bottom: 1px dashed #ddd;
}
.comment-head {
font-size: 12px;
margin-bottom: 3px;
}
.comment-time {
color: #999;
}
.comment-body {
font-size: 13px;
}
.comment-form {
margin-top: 12px;
}
.form-row {
margin-bottom: 8px;
}
.form-row label {
display: block;
font-size: 12px;
margin-bottom: 3px;
}
.form-row input,
.form-row textarea {
width: 100%;
padding: 6px 8px;
border: 1px solid #ccc;
font-size: 13px;
font-family: inherit;
}
.honeypot {
position: absolute;
left: -9999px;
opacity: 0;
}
.btn {
background: #1c62b0;
color: #fff;
border: none;
padding: 7px 18px;
font-size: 13px;
cursor: pointer;
}
.btn:hover {
background: #f60;
}
.error {
color: #c00;
font-size: 12px;
}
.alert {
padding: 8px 12px;
margin-bottom: 12px;
border: 1px solid;
}
.alert-success {
background: #e8f5e9;
border-color: #a5d6a7;
color: #2e7d32;
}
.alert-error {
background: #fdecea;
border-color: #f5c6cb;
color: #c62828;
}
.alert-warning {
background: #fff8e1;
border-color: #ffe082;
color: #8a6d00;
}
.empty {
color: #999;
text-align: center;
padding: 20px 0;
}
.list-title {
font-size: 18px;
margin-bottom: 12px;
color: #1c62b0;
}
.view-mode-switcher {
font-size: 12px;
color: #888;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px solid #d8dfe6;
}
.view-mode-switcher a.active {
font-weight: 700;
color: #1c62b0;
}
/* Sidebar */
.sidebar { width: 280px; flex-shrink: 0; }
.sidebar .searchbox { background: #fff; border: 1px solid #d8dfe6; padding: 10px; margin-bottom: 14px; }
.sidebar .searchbox input[type=search] { width: 180px; padding: 4px; border: 1px solid #ccc; }
.sidebar .widget { background: #fff; border: 1px solid #d8dfe6; margin-bottom: 14px; }
.sidebar .widget h3 { background: #f7f9fb; border-bottom: 1px solid #d8dfe6; font-size: 13px; padding: 7px 12px; color: #1c62b0; }
.sidebar .widget ul { list-style: none; padding: 6px 12px; }
.sidebar .widget li { padding: 3px 0; font-size: 12px; border-bottom: 1px dotted #eee; }
.sidebar .tagcloud { padding: 8px 12px; }
.sidebar .tagcloud a { color: #1c62b0; margin-right: 6px; }
.sidebar {
width: auto;
min-width: 0;
}
.sidebar .searchbox {
background: #fff;
border: 1px solid #d8dfe6;
padding: 10px;
margin-bottom: 14px;
}
.sidebar .searchbox input[type="search"] {
width: 180px;
padding: 4px;
border: 1px solid #ccc;
}
.sidebar .widget {
background: #fff;
border: 1px solid #d8dfe6;
margin-bottom: 14px;
}
.sidebar .widget h3 {
background: #f7f9fb;
border-bottom: 1px solid #d8dfe6;
font-size: 13px;
padding: 7px 12px;
color: #1c62b0;
}
.sidebar .widget ul {
list-style: none;
padding: 6px 12px;
}
.sidebar .widget li {
padding: 3px 0;
font-size: 12px;
border-bottom: 1px dotted #eee;
}
.sidebar .tagcloud {
padding: 8px 12px;
}
.sidebar .tagcloud a {
color: #1c62b0;
margin-right: 6px;
}
/* 兜底页面(归档/标签云/友链/评论/登录等使用默认布局) */
.container.main-layout { max-width: 960px; margin: 0 auto; }
.main-layout { display: flex; gap: 18px; align-items: flex-start; padding: 14px 10px; }
.main-layout .content { flex: 1; min-width: 0; }
.main-layout .sidebar { width: 280px; }
.container.main-layout {
max-width: 960px;
margin: 0 auto;
}
.main-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 280px;
gap: 18px;
align-items: start;
padding: 14px 10px;
}
.main-layout .content {
min-width: 0;
max-width: 100%;
}
.main-layout .sidebar {
width: auto;
min-width: 0;
}
.pagination-links { display: flex; gap: 6px; flex-wrap: wrap; margin: 12px 0; }
.pagination-links a, .pagination-links .current, .pagination-links .disabled { padding: 4px 10px; border: 1px solid #d8dfe6; background: #fff; font-size: 12px; }
.pagination-links .current { background: #1c62b0; color: #fff; }
.pagination {
width: 100%;
max-width: 100%;
min-width: 0;
margin: 12px 0;
}
.pagination-links {
display: flex;
gap: 4px;
flex-wrap: wrap;
align-items: center;
max-width: 100%;
margin: 12px 0;
}
.pagination-info {
color: #888;
font-size: 12px;
margin-right: 6px;
white-space: nowrap;
}
.pagination-links a,
.pagination-links .current,
.pagination-links .disabled {
padding: 2px 7px;
border: 1px solid #d8dfe6;
background: #fff;
font-size: 12px;
line-height: 1.4;
}
.pagination-links .current {
background: #1c62b0;
color: #fff;
}
#footer { clear: both; text-align: center; padding: 14px 0 24px; color: #888; font-size: 12px; }
#footer a { color: #1c62b0; }
#footer {
clear: both;
text-align: center;
padding: 14px 0 24px;
color: #888;
font-size: 12px;
}
#footer a {
color: #1c62b0;
}
.archive-year { font-size: 15px; margin: 12px 0 4px; color: #1c62b0; }
.archive-list { list-style: none; }
.archive-posts { margin: 4px 0 10px 20px; }
.archive-year {
font-size: 15px;
margin: 12px 0 4px;
color: #1c62b0;
}
.archive-list {
list-style: none;
}
.archive-posts {
margin: 4px 0 10px 20px;
}
.links-list { list-style: none; }
.links-list li { padding: 5px 0; }
.link-note { color: #999; }
.links-list {
list-style: none;
}
.links-list li {
padding: 5px 0;
}
.link-note {
color: #999;
}
.tag-cloud.big { line-height: 2.4; }
.tag-cloud.big {
line-height: 2.4;
}
.auth-layout { display: flex; justify-content: center; padding: 30px 10px; }
.auth-box { width: 100%; max-width: 420px; }
.auth-alt { margin-top: 12px; font-size: 12px; }
.search-form { margin-bottom: 12px; }
.search-form input { padding: 5px; border: 1px solid #ccc; width: 220px; }
.auth-layout {
display: flex;
justify-content: center;
padding: 30px 10px;
}
.auth-box {
width: 100%;
max-width: 420px;
}
.auth-alt {
margin-top: 12px;
font-size: 12px;
}
.search-form {
margin-bottom: 12px;
}
.search-form input {
padding: 5px;
border: 1px solid #ccc;
width: 220px;
}
.inline-form { display: inline; }
.link-btn { background: none; border: none; color: #1c62b0; cursor: pointer; font-size: 13px; padding: 0; font-weight: bold; }
.inline-form {
display: inline;
}
.link-btn {
background: none;
border: none;
color: #1c62b0;
cursor: pointer;
font-size: 13px;
padding: 0;
font-weight: bold;
}
/* ============ 移动端适配 ============ */
@media (max-width: 768px) {
#outmain { width: 100%; }
#header { padding: 14px 10px; }
.logo a { font-size: 24px; }
#nav ul { flex-wrap: wrap; }
#nav a { padding: 8px 12px; font-size: 13px; }
#page { flex-direction: column; }
#content { width: 100%; }
.sidebar { width: 100%; }
.main-layout { flex-direction: column; }
.main-layout .content { width: 100%; }
.main-layout .sidebar { width: 100%; }
.sidebar .searchbox input[type=search] { width: 70%; }
.post-card, .post-full, .comments-section { padding: 14px 16px; }
.post-cover { margin: -14px -16px 12px; }
.post-full .post-title { font-size: 22px; }
.archive-posts { margin-left: 10px; }
#outmain {
width: 100%;
}
#header {
padding: 14px 10px;
}
.logo a {
font-size: 24px;
}
#nav ul {
flex-wrap: wrap;
}
#nav a {
padding: 8px 12px;
font-size: 13px;
}
#page {
grid-template-columns: minmax(0, 1fr);
}
#content {
width: 100%;
}
.sidebar {
width: 100%;
}
.main-layout {
grid-template-columns: minmax(0, 1fr);
}
.main-layout .content {
width: 100%;
}
.main-layout .sidebar {
width: 100%;
}
.sidebar .searchbox input[type="search"] {
width: 70%;
}
.post-card,
.post-full,
.comments-section {
padding: 14px 16px;
}
.post-cover {
margin: -14px -16px 12px;
}
.post-full .post-title {
font-size: 22px;
}
.archive-posts {
margin-left: 10px;
}
}
/* TOC */
.toc { background: #f7f9fb; border: 1px solid #d8dfe6; padding: 10px 14px; margin: 0 0 14px; font-size: 13px; }
.toc summary { cursor: pointer; font-weight: bold; margin-bottom: 4px; }
.toc-list { list-style: none; }
.toc-list li { padding: 2px 0; }
.toc-level-3 { padding-left: 14px; }
.toc-level-4, .toc-level-5, .toc-level-6 { padding-left: 28px; }
.toc {
background: #f7f9fb;
border: 1px solid #d8dfe6;
padding: 10px 14px;
margin: 0 0 14px;
font-size: 13px;
}
.toc summary {
cursor: pointer;
font-weight: bold;
margin-bottom: 4px;
}
.toc-list {
list-style: none;
}
.toc-list li {
padding: 2px 0;
}
.toc-level-3 {
padding-left: 14px;
}
.toc-level-4,
.toc-level-5,
.toc-level-6 {
padding-left: 28px;
}
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
/* Paywall */
.paywall { border: 1px solid #e0e0e0; border-radius: 8px; padding: 24px 20px; background: #fafafa; text-align: center; }
.paywall-cover img { max-width: 100%; border-radius: 6px; margin-bottom: 14px; }
.paywall-title { font-size: 20px; margin: 0 0 10px; }
.paywall-excerpt { color: #666; font-size: 14px; margin: 0 0 14px; }
.paywall-price { font-size: 22px; font-weight: 800; color: #f60; margin: 0 0 8px; }
.paywall-hint { color: #888; font-size: 13px; margin: 0 0 16px; }
.paywall-actions .btn { margin: 0 6px; }
.paid-teaser { border: 1px dashed #f5c6cb; background: #fdecea; color: #c62828; border-radius: 6px; padding: 12px 14px; margin: 12px 0; font-size: 14px; }
.paid-teaser .btn { margin-left: 8px; }
.paywall {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 24px 20px;
background: #fafafa;
text-align: center;
}
.paywall-cover img {
max-width: 100%;
border-radius: 6px;
margin-bottom: 14px;
}
.paywall-title {
font-size: 20px;
margin: 0 0 10px;
}
.paywall-excerpt {
color: #666;
font-size: 14px;
margin: 0 0 14px;
}
.paywall-price {
font-size: 22px;
font-weight: 800;
color: #f60;
margin: 0 0 8px;
}
.paywall-hint {
color: #888;
font-size: 13px;
margin: 0 0 16px;
}
.paywall-actions .btn {
margin: 0 6px;
}
.paid-teaser {
border: 1px dashed #f5c6cb;
background: #fdecea;
color: #c62828;
border-radius: 6px;
padding: 12px 14px;
margin: 12px 0;
font-size: 14px;
}
.paid-teaser .btn {
margin-left: 8px;
}
/* 阅读全文链接 */
.read-more { display: inline-block; margin-top: 8px; font-size: 13px; color: #1c62b0; }
.read-more:hover { color: #f60; }
.read-more {
display: inline-block;
margin-top: 8px;
font-size: 13px;
color: #1c62b0;
}
.read-more:hover {
color: #f60;
}
/* 社交账号:横向胶囊列表 */
.social-links { list-style: none; display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; }
.social-links li { font-size: 12px; background: #f7f9fb; border: 1px solid #d8dfe6; border-radius: 999px; padding: 3px 10px; }
.social-links {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 0;
padding: 0;
}
.social-links li {
font-size: 12px;
background: #f7f9fb;
border: 1px solid #d8dfe6;
border-radius: 999px;
padding: 3px 10px;
}
/* 已解锁的付费内容块 */
.paid-content { border: 1px dashed #f5c6cb; border-left: 4px solid #f60; background: #fff8f0; border-radius: 6px; padding: 12px 14px; margin: 12px 0; }
.paid-content-label { font-size: 12px; color: #f60; font-weight: 700; margin-bottom: 6px; }
.paid-content {
border: 1px dashed #f5c6cb;
border-left: 4px solid #f60;
background: #fff8f0;
border-radius: 6px;
padding: 12px 14px;
margin: 12px 0;
}
.paid-content-label {
font-size: 12px;
color: #f60;
font-weight: 700;
margin-bottom: 6px;
}
/* 分类页介绍区 */
.category-intro { display: flex; gap: 14px; align-items: flex-start; background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; padding: 14px 16px; margin-bottom: 16px; }
.category-cover { width: 120px; height: 80px; object-fit: cover; border-radius: 6px; flex-shrink: 0; }
.category-description { margin: 0 0 6px; font-size: 14px; color: #555; line-height: 1.7; }
.category-meta { margin: 0; font-size: 12px; color: #999; }
.category-intro {
display: flex;
gap: 14px;
align-items: flex-start;
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 14px 16px;
margin-bottom: 16px;
}
.category-cover {
width: 120px;
height: 80px;
object-fit: cover;
border-radius: 6px;
flex-shrink: 0;
}
.category-description {
margin: 0 0 6px;
font-size: 14px;
color: #555;
line-height: 1.7;
}
.category-meta {
margin: 0;
font-size: 12px;
color: #999;
}
+2
View File
@@ -11,6 +11,8 @@
<div class="alert alert-error">{{ session('error') }}</div>
@endif
@include('partials.view-mode-switcher')
@forelse($posts as $post)
@include('partials.post-card', ['post' => $post])
@empty
+2
View File
@@ -24,6 +24,8 @@
@endif
@include('partials.view-mode-switcher')
@forelse($posts as $post)
@include('partials.post-card', ['post' => $post])
@empty
@@ -3,6 +3,7 @@
<p>&copy; {{ date('Y') }} {{ $siteName }}
@if($siteIcp)<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">{{ $siteIcp }}</a>@endif
</p>
<p>@include('partials.feed-links')</p>
<p>Powered by LaraLog · Laravel · Filament</p>
</footer>
</div>{{-- #outmain --}}
@@ -12,6 +12,7 @@
<meta name="generator" content="LaraLog">
<link rel="canonical" href="{{ url()->current() }}">
<link rel="alternate" type="application/rss+xml" title="{{ $siteName }}" href="{{ route('feed.rss') }}">
<link rel="alternate" type="text/plain" title="llms.txt" href="{{ route('feed.llms') }}">
<link rel="stylesheet" href="{{ theme('asset', 'style.css') }}">
@stack('head')
@stack('theme:head')
@@ -12,7 +12,7 @@
<li class="{{ request()->routeIs('tags*') ? 'current_page_item' : '' }}"><a href="{{ route('tags') }}">标签</a></li>
<li class="{{ request()->routeIs('links') ? 'current_page_item' : '' }}"><a href="{{ route('links') }}">友链</a></li>
<li class="{{ request()->routeIs('comments') ? 'current_page_item' : '' }}"><a href="{{ route('comments') }}">评论</a></li>
<li><a href="{{ route('feed.rss') }}">RSS</a></li>
@include('partials.feed-links', ['asList' => true])
@auth
<li><a href="{{ route('profile') }}">{{ auth()->user()->name }}</a></li>
<li>
+2
View File
@@ -10,6 +10,8 @@
<button type="submit" class="btn">搜索</button>
</form>
@include('partials.view-mode-switcher')
@forelse($posts as $post)
@include('partials.post-card', ['post' => $post])
@empty
+6 -1
View File
@@ -14,6 +14,7 @@
@if(session('error'))
<div class="alert alert-error">{{ session('error') }}</div>
@endif
@include('partials.post-preview-banner')
<article class="post-full">
@if(! empty($toc))
@@ -35,7 +36,11 @@
@endif
<h1 class="post-title">{{ $post->title }}</h1>
<div class="post-meta">
<time datetime="{{ $post->published_at->toIso8601String() }}">{{ $post->published_at->format(blog_setting('normaltime', 'Y-m-d H:i')) }}</time>
@if($post->published_at)
<time datetime="{{ $post->published_at->toIso8601String() }}">{{ $post->published_at->format(blog_setting('normaltime', 'Y-m-d H:i')) }}</time>
@else
<span>未发布</span>
@endif
@if($post->author)<span>· {{ $post->author->name }}</span>@endif
@if($post->category)
<span>· <a href="{{ route('category.show', $post->category->id) }}">{{ $post->category->name }}</a></span>
+2
View File
@@ -6,6 +6,8 @@
<main id="content">
<h1 class="list-title">标签:{{ $tag->name }}</h1>
@include('partials.view-mode-switcher')
@forelse($posts as $post)
@include('partials.post-card', ['post' => $post])
@empty