visuddhinanda před 5 dny
rodič
revize
5e5fa4c16d

+ 208 - 0
api-v12/app/Http/Controllers/Library/AnthologyController.php

@@ -0,0 +1,208 @@
+<?php
+
+namespace App\Http\Controllers\Library;
+
+use App\Http\Controllers\Controller;
+use App\Services\CollectionService;
+use Illuminate\Http\Request;
+
+class AnthologyController extends Controller
+{
+    // 封面渐变色池:uid 首字节取余循环,保证同一文集颜色稳定
+    private array $coverGradients = [
+        'linear-gradient(160deg, #2d2010, #4a3010)',
+        'linear-gradient(160deg, #1a2d10, #2d4a18)',
+        'linear-gradient(160deg, #0d1f3c, #1a3660)',
+        'linear-gradient(160deg, #2d1020, #4a1830)',
+        'linear-gradient(160deg, #1a1a2d, #2a2a50)',
+        'linear-gradient(160deg, #1a2820, #2d4438)',
+    ];
+
+    // 作者色池:同上,根据 studio.id 首字节取余
+    private array $authorColors = [
+        '#c8860a', '#2e7d32', '#1565c0', '#6a1b9a',
+        '#c62828', '#00695c', '#4e342e', '#37474f',
+    ];
+
+    public function __construct(private CollectionService $collectionService) {}
+
+    // -------------------------------------------------------------------------
+    // 从 uid / id 字符串中提取一个稳定的整数,用于色池取余
+    // -------------------------------------------------------------------------
+    private function colorIndex(string $uid): int
+    {
+        return hexdec(substr(str_replace('-', '', $uid), 0, 4)) % 255;
+    }
+
+    // -------------------------------------------------------------------------
+    // 将 studio 对象转换为 blade 所需的 author 数组
+    // -------------------------------------------------------------------------
+    private function formatAuthor(array $studio): array
+    {
+        $name     = $studio['nickName'] ?? $studio['studioName'] ?? '未知';
+        $initials = mb_substr($name, 0, 2);
+        $colorIdx = $this->colorIndex($studio['id'] ?? '0');
+
+        return [
+            'name'     => $name,
+            'initials' => $initials,
+            'color'    => $this->authorColors[$colorIdx % count($this->authorColors)],
+            'avatar'   => $studio['avatar'] ?? null,
+        ];
+    }
+
+    // -------------------------------------------------------------------------
+    // 将 CollectionResource 单条转换为 index 卡片所需格式
+    // -------------------------------------------------------------------------
+    private function formatForCard(array $item, int $index): array
+    {
+        $uid      = $item['uid'];
+        $colorIdx = $this->colorIndex($uid);
+
+        // article_list => 章节标题列表
+        $chapters = collect($item['article_list'] ?? [])
+            ->pluck('title')
+            ->toArray();
+
+        return [
+            'id'             => $uid,
+            'title'          => $item['title'],
+            'subtitle'       => $item['subtitle'] ?? null,
+            'description'    => $item['summary'] ?? null,
+            'cover_image'    => null, // 暂无封面图字段,留空走渐变
+            'cover_gradient' => $this->coverGradients[$colorIdx % count($this->coverGradients)],
+            'author'         => $this->formatAuthor($item['studio'] ?? []),
+            'chapters'       => $chapters,
+            'children_number'=> $item['childrenNumber'] ?? count($chapters),
+            'updated_at'     => isset($item['updated_at'])
+                ? \Carbon\Carbon::parse($item['updated_at'])->format('Y-m-d')
+                : '',
+        ];
+    }
+
+    // =========================================================================
+    // index
+    // =========================================================================
+    public function index(Request $request)
+    {
+        $perPage     = 10;
+        $currentPage = (int) $request->get('page', 1);
+
+        $result = $this->collectionService->getPublicList($perPage, $currentPage);
+
+        // $result['data'] 是 CollectionResource collection,转为数组逐条加工
+        $items = collect($result['data'])
+            ->values()
+            ->map(fn ($item, $i) => $this->formatForCard(
+                is_array($item) ? $item : $item->toArray(request()),
+                $i
+            ));
+
+        $total = $result['total'];
+
+        $paginator = new \Illuminate\Pagination\LengthAwarePaginator(
+            $items,
+            $total,
+            $perPage,
+            $currentPage,
+            ['path' => $request->url(), 'query' => $request->query()]
+        );
+
+        // 侧边栏作者列表:从当页数据聚合(如需完整列表可单独查询)
+        $authors = $items
+            ->groupBy(fn ($i) => $i['author']['name'])
+            ->map(fn ($group, $name) => [
+                'name'     => $name,
+                'initials' => $group->first()['author']['initials'],
+                'color'    => $group->first()['author']['color'],
+                'avatar'   => $group->first()['author']['avatar'],
+                'count'    => $group->count(),
+            ])
+            ->values();
+
+        return view('library.anthology.index', [
+            'anthologies' => $paginator,
+            'authors'     => $authors,
+            'total'       => $total,
+        ]);
+    }
+
+    // =========================================================================
+    // show
+    // =========================================================================
+    public function show(string $uid)
+    {
+        $result = $this->collectionService->getCollection($uid);
+
+        if (isset($result['error'])) {
+            abort($result['code'] ?? 404, $result['error']);
+        }
+
+        $raw = is_array($result['data'])
+            ? $result['data']
+            : $result['data']->toArray(request());
+
+        $colorIdx = $this->colorIndex($raw['uid']);
+        $author   = $this->formatAuthor($raw['studio'] ?? []);
+
+        // 只保留 level=1 的顶级章节
+        $articles = collect($raw['article_list'] ?? [])
+            ->filter(fn ($a) => ($a['level'] ?? 1) === 1)
+            ->values()
+            ->map(fn ($a, $i) => [
+                'id'    => $a['article_id'],
+                'order' => $i + 1,
+                'title' => $a['title'],
+            ])
+            ->toArray();
+
+        $anthology = [
+            'id'             => $raw['uid'],
+            'title'          => $raw['title'],
+            'subtitle'       => $raw['subtitle'] ?? null,
+            'description'    => $raw['summary'] ?? $raw['subtitle'] ?? null,
+            'about'          => $raw['summary'] ?? null,
+            'cover_image'    => null,
+            'cover_gradient' => $this->coverGradients[$colorIdx % count($this->coverGradients)],
+            'tags'           => array_filter([$raw['lang'] ?? null]),
+            'language'       => $raw['lang'] ?? null,
+            'category'       => null,
+            'created_at'     => isset($raw['created_at'])
+                ? \Carbon\Carbon::parse($raw['created_at'])->format('Y-m-d')
+                : '',
+            'updated_at'     => isset($raw['updated_at'])
+                ? \Carbon\Carbon::parse($raw['updated_at'])->format('Y-m-d')
+                : '',
+            'children_number' => $raw['childrenNumber'] ?? 0,
+            'author'         => array_merge($author, [
+                'bio'           => null,
+                'article_count' => $raw['childrenNumber'] ?? 0,
+            ]),
+            'articles'       => $articles,
+        ];
+
+        // 相关文集:同作者其他文集
+        $relatedResult = $this->collectionService->getPublicList(20, 1);
+        $related = collect($relatedResult['data'])
+            ->map(fn ($item) => is_array($item) ? $item : $item->toArray(request()))
+            ->filter(fn ($item) =>
+                $item['uid'] !== $uid &&
+                ($item['studio']['id'] ?? '') === ($raw['studio']['id'] ?? '')
+            )
+            ->take(3)
+            ->map(fn ($item) => [
+                'id'             => $item['uid'],
+                'title'          => $item['title'],
+                'author_name'    => $item['studio']['nickName'] ?? $item['studio']['studioName'] ?? '',
+                'cover_gradient' => $this->coverGradients[
+                    $this->colorIndex($item['uid']) % count($this->coverGradients)
+                ],
+            ])
+            ->values();
+
+        return view('library.anthology.show', [
+            'anthology' => $anthology,
+            'related'   => $related,
+        ]);
+    }
+}

+ 144 - 0
api-v12/app/Http/Controllers/Library/AnthologyReadController.php

@@ -0,0 +1,144 @@
+<?php
+
+namespace App\Http\Controllers\Library;
+
+use App\Http\Controllers\Controller;
+use App\Services\CollectionService;
+use App\Services\ArticleService;
+use Illuminate\Http\Request;
+
+class AnthologyReadController extends Controller
+{
+    public function __construct(
+        private CollectionService $collectionService,
+        private ArticleService    $articleService,
+    ) {}
+
+    // =========================================================================
+    // read
+    // GET /library/anthology/{anthology}/read/{article}
+    // =========================================================================
+    public function read(string $anthologyId, string $articleId)
+    {
+        // ── 1. 获取文集信息 ───────────────────────────────────────────────────
+        $colResult = $this->collectionService->getCollection($anthologyId);
+        if (isset($colResult['error'])) {
+            abort($colResult['code'] ?? 404, $colResult['error']);
+        }
+
+        $col = is_array($colResult['data'])
+            ? $colResult['data']
+            : $colResult['data']->toArray(request());
+
+        // ── 2. 构建完整目录(所有 level,保留层级信息) ────────────────────
+        $fullArticleList = collect($col['article_list'] ?? []);
+
+        // toc:只取 level=1 的顶级节点,交给阅读页左侧目录使用
+        // 与 book.read 的 toc 格式保持一致
+        $toc = $fullArticleList
+            ->filter(fn($a) => ($a['level'] ?? 1) === 1)
+            ->values()
+            ->map(fn($a) => [
+                'id'       => $a['article_id'],
+                'title'    => $a['title'],
+                'summary'  => '',
+                'progress' => 0,
+                'level'    => (int) ($a['level'] ?? 1),
+                'disabled' => false,
+            ])
+            ->toArray();
+
+        // ── 3. 获取当前文章内容 ───────────────────────────────────────────────
+        $artResult = $this->articleService->getArticle($articleId);
+        if (isset($artResult['error'])) {
+            abort($artResult['code'] ?? 404, $artResult['error']);
+        }
+
+        // ArticleResource 需要 format=html
+        $fakeRequest = Request::create('', 'GET', ['format' => 'html']);
+        $artResource = $artResult['data'];
+        $artArray    = $artResource->toArray($fakeRequest);
+
+        // content 统一包装成 book.read 期望的格式:
+        // [ ['id'=>..., 'level'=>1, 'text'=>[[ html_string ]]], ... ]
+        $content = [[
+            'id'    => $articleId,
+            'level' => 100,
+            'text'  => [[$artArray['html'] ?? $artArray['content']]],
+        ]];
+
+        // ── 4. 计算翻页(pagination) ─────────────────────────────────────────
+        // 只在 level=1 的节点间翻页(与目录一致)
+        $level1 = $fullArticleList
+            ->filter(fn($a) => ($a['level'] ?? 1) === 1)
+            ->values();
+
+        $currentIndex = $level1->search(
+            fn($a) => $a['article_id'] === $articleId
+        );
+
+        $prev = null;
+        $next = null;
+
+        if ($currentIndex !== false) {
+            if ($currentIndex > 0) {
+                $prevItem = $level1[$currentIndex - 1];
+                $prev = [
+                    'id'    => $prevItem['article_id'],
+                    'title' => $prevItem['title'],
+                ];
+            }
+            if ($currentIndex < $level1->count() - 1) {
+                $nextItem = $level1[$currentIndex + 1];
+                $next = [
+                    'id'    => $nextItem['article_id'],
+                    'title' => $nextItem['title'],
+                ];
+            }
+        }
+
+        $pagination = [
+            'start' => 0,
+            'end'   => 0,
+            'prev'  => $prev,
+            'next'  => $next,
+        ];
+
+        // ── 5. 组装 $book(对齐 book.read 的数据结构) ───────────────────────
+        $studio = $col['studio'] ?? [];
+
+        // blade 用 $book['publisher']->username / ->nickname,必须是对象
+        $publisher = (object) [
+            'username' => $studio['studioName'] ?? '',
+            'nickname' => $studio['nickName']   ?? $studio['studioName'] ?? '',
+        ];
+
+        $book = [
+            'id'          => $articleId,
+            'title'       => $artArray['title'] ?? ($col['title'] ?? ''),
+            'author'      => $studio['nickName'] ?? $studio['studioName'] ?? '',
+            'publisher'   => $publisher,
+            'type'        => '',
+            'category_id' => null,
+            'cover'       => null,
+            'description' => $col['summary'] ?? '',
+            'language'    => $col['lang'] ?? '',
+            'anthology'   => [
+                'id'    => $anthologyId,
+                'title' => $col['title'] ?? '',
+            ],
+            'categories'  => [],
+            'tags'        => [],
+            'downloads'   => [],
+            'toc'         => $toc,
+            'pagination'  => $pagination,
+            'content'     => $content,
+        ];
+
+        // blade 里有 $relatedBooks,传空数组防止 undefined variable
+        $relatedBooks = [];
+
+        // 翻页路由需要 anthologyId,传给 blade 供覆盖路由使用
+        return view('library.book.read', compact('book', 'relatedBooks', 'anthologyId'));
+    }
+}

+ 202 - 0
api-v12/app/Services/CollectionService.php

@@ -0,0 +1,202 @@
+<?php
+
+namespace App\Services;
+
+use App\Models\Collection;
+use App\Http\Api\AuthApi;
+use App\Http\Api\StudioApi;
+use App\Http\Api\ShareApi;
+use Illuminate\Http\Request;
+use App\Http\Resources\CollectionResource;
+use Illuminate\Support\Facades\Log;
+
+class CollectionService
+{
+    protected $indexCol = [
+        'uid',
+        'title',
+        'subtitle',
+        'summary',
+        'article_list',
+        'owner',
+        'status',
+        'default_channel',
+        'lang',
+        'updated_at',
+        'created_at',
+    ];
+    /**
+     * 判断用户是否有编辑权限
+     */
+    public function userCanEdit(string $user_uid, Collection $collection): bool
+    {
+        if ($collection->owner === $user_uid) {
+            return true;
+        }
+
+        return ShareApi::getResPower($user_uid, $collection->uid) >= 20;
+    }
+
+    /**
+     * 判断用户是否有读取权限
+     */
+    public function userCanRead(string $user_uid, Collection $collection): bool
+    {
+        if ($collection->owner === $user_uid) {
+            return true;
+        }
+
+        return ShareApi::getResPower($user_uid, $collection->uid) >= 10;
+    }
+
+    /**
+     * 获取当前 studio 下我的数量与协作数量
+     */
+    public function getMyNumber(Request $request): array
+    {
+        $user = AuthApi::current($request);
+        if (!$user) {
+            return ['error' => __('auth.failed'), 'code' => 403];
+        }
+
+        $studioId = StudioApi::getIdByName($request->get('studio'));
+        if ($user['user_uid'] !== $studioId) {
+            return ['error' => __('auth.failed'), 'code' => 403];
+        }
+
+        $my = Collection::where('owner', $studioId)->count();
+
+        $resList = ShareApi::getResList($studioId, 4);
+        $resId = array_column($resList, 'res_id');
+
+        $collaboration = Collection::whereIn('uid', $resId)
+            ->where('owner', '<>', $studioId)
+            ->count();
+
+        return ['data' => ['my' => $my, 'collaboration' => $collaboration]];
+    }
+
+    /**
+     * 构建 index 查询,根据 view 参数分发不同逻辑
+     */
+    public function buildIndexQuery(Request $request): array
+    {
+
+        $table = match ($request->get('view')) {
+            'studio_list' => $this->buildStudioListQuery($this->indexCol),
+            'studio'      => $this->buildStudioQuery($request, $this->indexCol),
+            'public'      => $this->buildPublicQuery($request, $this->indexCol),
+            default       => null,
+        };
+
+        if ($table === null) {
+            return ['error' => '无法识别的view参数'];
+        }
+
+        // 如果是鉴权失败从 studio 分支返回的错误
+        if (isset($table['error'])) {
+            return $table;
+        }
+
+        // 搜索
+        if ($request->filled('search')) {
+            $table = $table->where('title', 'like', '%' . $request->get('search') . '%');
+        }
+
+        $count = $table->count();
+
+        // 排序
+        if ($request->has('order') && $request->has('dir')) {
+            $table = $table->orderBy($request->get('order'), $request->get('dir'));
+        } else {
+            $orderCol = $request->get('view') === 'studio_list' ? 'count' : 'updated_at';
+            $table = $table->orderBy($orderCol, 'desc');
+        }
+
+        $result = $table
+            ->skip($request->get('offset', 0))
+            ->take($request->get('limit', 1000))
+            ->get();
+
+        return ['data' => $result, 'count' => $count];
+    }
+
+    // -------------------------------------------------------------------------
+    // 私有:各 view 的查询构建
+    // -------------------------------------------------------------------------
+
+    private function buildStudioListQuery(array $indexCol)
+    {
+        return Collection::select(['owner'])
+            ->selectRaw('count(*) as count')
+            ->where('status', 30)
+            ->groupBy('owner');
+    }
+
+    private function buildStudioQuery(Request $request, array $indexCol): array|\Illuminate\Database\Eloquent\Builder
+    {
+        $user = AuthApi::current($request);
+        if (!$user) {
+            return ['error' => __('auth.failed'), 'code' => 403];
+        }
+
+        $studioId = StudioApi::getIdByName($request->get('name'));
+        if ($user['user_uid'] !== $studioId) {
+            return ['error' => __('auth.failed'), 'code' => 403];
+        }
+
+        $table = Collection::select($indexCol);
+
+        if ($request->get('view2', 'my') === 'my') {
+            return $table->where('owner', $studioId);
+        }
+
+        // 协作
+        $resList = ShareApi::getResList($studioId, 4);
+        $resId = array_column($resList, 'res_id');
+
+        return $table->whereIn('uid', $resId)->where('owner', '<>', $studioId);
+    }
+
+    private function buildPublicQuery(Request $request, array $indexCol)
+    {
+        $table = Collection::select($indexCol)->where('status', 30);
+
+        if ($request->has('studio')) {
+            $studioId = StudioApi::getIdByName($request->get('studio'));
+            $table = $table->where('owner', $studioId);
+        }
+
+        return $table;
+    }
+
+    public function getPublicList(int $pageSize, int $currPage): array
+    {
+
+
+        $table = Collection::select($this->indexCol)->where('status', 30);
+
+        $count = $table->count();
+
+        $result = $table
+            ->orderBy('updated_at', 'desc')
+            ->skip(($currPage - 1) * $pageSize)
+            ->take($pageSize)
+            ->get();
+        return ['data' => CollectionResource::collection($result), 'total' => $count];
+    }
+
+    public function getCollection(string $id): array
+    {
+        $result = Collection::where('uid', $id)->first();
+        if (!$result) {
+            Log::warning("没有查询到数据 id={$id}");
+            return ['error' => "没有查询到数据 id={$id}", 'code' => 404];
+        }
+
+
+        $result->fullArticleList = true;
+
+        return ['data' => new CollectionResource($result)];
+    }
+}

+ 359 - 0
api-v12/resources/views/library/anthology/index.blade.php

@@ -0,0 +1,359 @@
+@extends('library.layouts.app')
+
+@section('title', '文集 · 巴利书库')
+
+@push('styles')
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;600;700&family=Noto+Sans+SC:wght@300;400;500&display=swap" rel="stylesheet">
+<style>
+:root {
+    --sf: #c8860a;
+    --sf-light: #f5e6c8;
+    --sf-pale: #fdf8f0;
+    --ink: #1a1208;
+    --ink-soft: #4a3f2f;
+    --ink-muted: #8a7a68;
+    --bdr: #e8ddd0;
+    --card-bg: #fffdf9;
+}
+body { background: var(--sf-pale) !important; font-family: 'Noto Sans SC', sans-serif; }
+
+/* Page header */
+.anthology-page-header {
+    background: linear-gradient(135deg, var(--ink) 0%, #2d2010 100%);
+    padding: 2.25rem 0 2rem;
+    position: relative;
+    overflow: hidden;
+    margin-bottom: 0;
+}
+.anthology-page-header::before {
+    content: '藏';
+    font-family: 'Noto Serif SC', serif;
+    font-size: 16rem;
+    font-weight: 700;
+    color: rgba(255,255,255,0.03);
+    position: absolute;
+    right: -1rem;
+    top: -2.5rem;
+    line-height: 1;
+    pointer-events: none;
+}
+.anthology-page-header h1 {
+    font-family: 'Noto Serif SC', serif;
+    font-size: 1.75rem;
+    font-weight: 600;
+    color: #fff;
+    margin: 0 0 0.3rem;
+    letter-spacing: 0.08em;
+}
+.anthology-page-header p { color: rgba(255,255,255,0.45); font-size: 0.85rem; margin: 0; }
+.result-badge {
+    background: var(--sf);
+    color: var(--ink);
+    font-size: 0.75rem;
+    font-weight: 700;
+    padding: 2px 9px;
+    border-radius: 20px;
+    margin-left: 0.6rem;
+    vertical-align: middle;
+}
+
+/* Card */
+.anthology-card {
+    background: var(--card-bg);
+    border: 1px solid var(--bdr);
+    border-radius: 10px;
+    overflow: hidden;
+    display: flex;
+    transition: box-shadow .25s, transform .25s;
+    margin-bottom: 1.1rem;
+    text-decoration: none;
+    color: inherit;
+}
+.anthology-card:hover {
+    box-shadow: 0 8px 28px rgba(200,134,10,.12), 0 2px 8px rgba(0,0,0,.06);
+    transform: translateY(-2px);
+    color: inherit;
+    text-decoration: none;
+}
+.card-cover {
+    width: 130px;
+    min-width: 130px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 1.1rem 0.7rem;
+    position: relative;
+    overflow: hidden;
+}
+.card-cover img {
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+    position: absolute;
+    inset: 0;
+}
+.card-cover::after {
+    content: '';
+    position: absolute;
+    inset: 0;
+    background: repeating-linear-gradient(45deg, transparent, transparent 8px, rgba(255,255,255,.015) 8px, rgba(255,255,255,.015) 9px);
+}
+.cover-text-wrap { position: relative; z-index: 1; text-align: center; }
+.cover-title {
+    font-family: 'Noto Serif SC', serif;
+    font-size: 1rem;
+    font-weight: 600;
+    color: #fff;
+    line-height: 1.6;
+    letter-spacing: .12em;
+    word-break: break-all;
+}
+.cover-divider { width: 28px; height: 1px; background: var(--sf); margin: .5rem auto; }
+.cover-sub { font-size: .65rem; color: rgba(255,255,255,.45); letter-spacing: .04em; }
+
+.card-body-inner { padding: 1.1rem 1.4rem; flex: 1; display: flex; flex-direction: column; }
+.card-main-title {
+    font-family: 'Noto Serif SC', serif;
+    font-size: 1.1rem;
+    font-weight: 600;
+    color: var(--ink);
+    margin-bottom: .35rem;
+    line-height: 1.4;
+}
+.anthology-card:hover .card-main-title { color: var(--sf); }
+.card-desc { font-size: .8rem; color: var(--ink-muted); margin-bottom: .65rem; line-height: 1.65; }
+.card-author { display: flex; align-items: center; gap: .45rem; margin-bottom: .7rem; }
+.a-avatar {
+    width: 24px; height: 24px; border-radius: 50%;
+    display: flex; align-items: center; justify-content: center;
+    font-size: .65rem; font-weight: 700; color: #fff; flex-shrink: 0;
+}
+.a-avatar-img {
+    width: 24px; height: 24px; border-radius: 50%;
+    object-fit: cover; flex-shrink: 0;
+}
+.a-name { font-size: .8rem; color: var(--ink-soft); font-weight: 500; }
+.chapter-tags { display: flex; flex-wrap: wrap; gap: .3rem; margin-top: auto; }
+.c-tag {
+    font-size: .7rem; color: var(--ink-muted);
+    background: var(--sf-light); border: 1px solid var(--bdr);
+    padding: 1px 7px; border-radius: 4px; white-space: nowrap;
+}
+.c-tag.more { background: transparent; border-color: transparent; color: var(--sf); }
+.card-meta-row {
+    display: flex; align-items: center; gap: .85rem;
+    margin-top: .65rem; padding-top: .65rem;
+    border-top: 1px solid var(--bdr);
+}
+.meta-it { font-size: .72rem; color: var(--ink-muted); display: flex; align-items: center; gap: .25rem; }
+
+/* Pagination — force horizontal */
+.anthology-pagination .pagination {
+    display: flex !important;
+    flex-direction: row !important;
+    flex-wrap: wrap;
+    gap: 3px;
+    justify-content: center;
+    list-style: none;
+    padding: 0;
+    margin: 0;
+}
+.anthology-pagination .page-item { display: inline-block; }
+.anthology-pagination .page-item .page-link {
+    border: 1px solid var(--bdr);
+    color: var(--ink-soft);
+    font-size: .82rem;
+    padding: 5px 11px;
+    background: var(--card-bg);
+    border-radius: 5px;
+    display: inline-block;
+    line-height: 1.5;
+    text-decoration: none;
+}
+.anthology-pagination .page-item.active .page-link { background: var(--sf); border-color: var(--sf); color: #fff; }
+.anthology-pagination .page-item .page-link:hover { background: var(--sf-light); color: var(--sf); }
+.anthology-pagination .page-item.disabled .page-link { opacity: .45; pointer-events: none; }
+
+/* Sidebar */
+.sidebar-box {
+    background: var(--card-bg);
+    border: 1px solid var(--bdr);
+    border-radius: 10px;
+    overflow: hidden;
+    margin-bottom: 1.1rem;
+}
+.sb-header {
+    padding: .75rem 1.15rem;
+    border-bottom: 1px solid var(--bdr);
+    font-family: 'Noto Serif SC', serif;
+    font-size: .875rem;
+    font-weight: 600;
+    color: var(--ink-soft);
+    letter-spacing: .04em;
+    display: flex;
+    align-items: center;
+    gap: .45rem;
+}
+.sb-header::before {
+    content: '';
+    display: block;
+    width: 3px;
+    height: 13px;
+    background: var(--sf);
+    border-radius: 2px;
+}
+.author-ul { list-style: none; padding: .35rem 0; margin: 0; }
+.author-ul li a {
+    display: flex; align-items: center; gap: .6rem;
+    padding: .45rem 1.15rem;
+    text-decoration: none;
+    transition: background .15s;
+}
+.author-ul li a:hover { background: var(--sf-pale); }
+.au-avatar {
+    width: 28px; height: 28px; border-radius: 50%;
+    display: flex; align-items: center; justify-content: center;
+    font-size: .68rem; font-weight: 700; color: #fff; flex-shrink: 0;
+}
+.au-avatar-img {
+    width: 28px; height: 28px; border-radius: 50%;
+    object-fit: cover; flex-shrink: 0;
+}
+.au-name { font-size: .8rem; color: var(--ink-soft); font-weight: 500; display: block; }
+.au-count { font-size: .7rem; color: var(--ink-muted); }
+
+/* Search */
+.search-wrap { background: var(--card-bg); border: 1px solid var(--bdr); border-radius: 10px; padding: .85rem 1.1rem; margin-bottom: 1.1rem; }
+.search-input {
+    width: 100%;
+    border: 1px solid var(--bdr);
+    border-radius: 6px;
+    padding: .45rem .9rem;
+    font-size: .83rem;
+    font-family: 'Noto Sans SC', sans-serif;
+    background: var(--sf-pale);
+    color: var(--ink);
+    outline: none;
+    transition: border-color .2s;
+}
+.search-input:focus { border-color: var(--sf); background: #fff; }
+.search-input::placeholder { color: var(--ink-muted); }
+
+@media (max-width:768px) {
+    .anthology-card { flex-direction: column; }
+    .card-cover { width: 100%; min-width: unset; height: 90px; }
+}
+</style>
+@endpush
+
+@section('content')
+<div class="anthology-page-header">
+    <div class="container-xl">
+        <h1>文集 <span class="result-badge">{{ $total }}</span></h1>
+        <p>经论注疏 · 禅修指引 · 法义探讨</p>
+    </div>
+</div>
+
+<div class="page-body" style="background: var(--sf-pale);">
+    <div class="container-xl">
+        <div class="row mt-3">
+
+            {{-- Left --}}
+            <div class="col-lg-9">
+
+                @forelse($anthologies as $item)
+                <a href="{{ route('library.anthology.show', $item['id']) }}" class="anthology-card">
+                    {{-- Cover --}}
+                    <div class="card-cover" style="{{ $item['cover_image'] ? '' : 'background: ' . $item['cover_gradient'] }}">
+                        @if($item['cover_image'])
+                            <img src="{{ $item['cover_image'] }}" alt="{{ $item['title'] }}">
+                        @else
+                            <div class="cover-text-wrap">
+                                <div class="cover-title">{{ $item['title'] }}</div>
+                                <div class="cover-divider"></div>
+                                <div class="cover-sub">{{ $item['subtitle'] ?? '' }}</div>
+                            </div>
+                        @endif
+                    </div>
+
+                    {{-- Body --}}
+                    <div class="card-body-inner">
+                        <div class="card-main-title">{{ $item['title'] }}</div>
+                        @if(!empty($item['description']))
+                        <div class="card-desc">{{ $item['description'] }}</div>
+                        @endif
+                        <div class="card-author">
+                            @if(!empty($item['author']['avatar']))
+                                <img src="{{ $item['author']['avatar'] }}" class="a-avatar-img" alt="">
+                            @else
+                                <div class="a-avatar" style="background: {{ $item['author']['color'] }}">
+                                    {{ $item['author']['initials'] }}
+                                </div>
+                            @endif
+                            <span class="a-name">{{ $item['author']['name'] }}</span>
+                        </div>
+                        <div class="chapter-tags">
+                            @foreach(array_slice($item['chapters'], 0, 4) as $ch)
+                            <span class="c-tag" title="{{ $ch }}">{{ mb_strimwidth($ch, 0, 14, '…') }}</span>
+                            @endforeach
+                            @if($item['children_number'] > 4)
+                            <span class="c-tag more">+{{ $item['children_number'] - 4 }} 章</span>
+                            @endif
+                        </div>
+                        <div class="card-meta-row">
+                            <span class="meta-it">
+                                <i class="ti ti-calendar" style="font-size:.82rem;"></i>
+                                {{ $item['updated_at'] }}
+                            </span>
+                            <span class="meta-it">
+                                <i class="ti ti-file-text" style="font-size:.82rem;"></i>
+                                {{ $item['children_number'] }} 章节
+                            </span>
+                        </div>
+                    </div>
+                </a>
+                @empty
+                <div class="text-center py-5 text-muted">暂无文集</div>
+                @endforelse
+
+                {{-- Pagination --}}
+                <div class="d-flex justify-content-center mt-3 anthology-pagination">
+                    {{ $anthologies->links('library.anthology.pagination') }}
+                </div>
+
+            </div>{{-- /col --}}
+
+            {{-- Sidebar --}}
+            <div class="col-lg-3">
+                <div class="search-wrap">
+                    <input type="text" class="search-input" placeholder="搜索文集…">
+                </div>
+
+                <div class="sidebar-box">
+                    <div class="sb-header">作者</div>
+                    <ul class="author-ul">
+                        @foreach($authors as $author)
+                        <li>
+                            <a href="#">
+                                @if(!empty($author['avatar']))
+                                    <img src="{{ $author['avatar'] }}" class="au-avatar-img" alt="">
+                                @else
+                                    <div class="au-avatar" style="background: {{ $author['color'] }}">{{ $author['initials'] }}</div>
+                                @endif
+                                <div>
+                                    <span class="au-name">{{ $author['name'] }}</span>
+                                    <span class="au-count">{{ $author['count'] }} 篇文集</span>
+                                </div>
+                            </a>
+                        </li>
+                        @endforeach
+                    </ul>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>
+@endsection

+ 33 - 0
api-v12/resources/views/library/anthology/pagination.blade.php

@@ -0,0 +1,33 @@
+@if ($paginator->hasPages())
+<ul class="pagination">
+    {{-- Previous --}}
+    @if ($paginator->onFirstPage())
+        <li class="page-item disabled"><span class="page-link">«</span></li>
+    @else
+        <li class="page-item"><a class="page-link" href="{{ $paginator->previousPageUrl() }}">«</a></li>
+    @endif
+
+    {{-- Pages --}}
+    @foreach ($elements as $element)
+        @if (is_string($element))
+            <li class="page-item disabled"><span class="page-link">{{ $element }}</span></li>
+        @endif
+        @if (is_array($element))
+            @foreach ($element as $page => $url)
+                @if ($page == $paginator->currentPage())
+                    <li class="page-item active"><span class="page-link">{{ $page }}</span></li>
+                @else
+                    <li class="page-item"><a class="page-link" href="{{ $url }}">{{ $page }}</a></li>
+                @endif
+            @endforeach
+        @endif
+    @endforeach
+
+    {{-- Next --}}
+    @if ($paginator->hasMorePages())
+        <li class="page-item"><a class="page-link" href="{{ $paginator->nextPageUrl() }}">»</a></li>
+    @else
+        <li class="page-item disabled"><span class="page-link">»</span></li>
+    @endif
+</ul>
+@endif

+ 473 - 0
api-v12/resources/views/library/anthology/show.blade.php

@@ -0,0 +1,473 @@
+@extends('library.layouts.app')
+
+@section('title', $anthology['title'] . ' · 巴利书库')
+
+@push('styles')
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;600;700&family=Noto+Sans+SC:wght@300;400;500&display=swap" rel="stylesheet">
+<style>
+:root {
+    --sf: #c8860a;
+    --sf-light: #f5e6c8;
+    --sf-pale: #fdf8f0;
+    --ink: #1a1208;
+    --ink-soft: #4a3f2f;
+    --ink-muted: #8a7a68;
+    --bdr: #e8ddd0;
+    --card-bg: #fffdf9;
+}
+body { background: var(--sf-pale) !important; font-family: 'Noto Sans SC', sans-serif; }
+
+/* Breadcrumb bar */
+.anthology-breadcrumb-bar {
+    background: rgba(255,255,255,.55);
+    border-bottom: 1px solid var(--bdr);
+    padding: .5rem 0;
+}
+.anthology-breadcrumb-bar .breadcrumb { margin: 0; font-size: .78rem; }
+.anthology-breadcrumb-bar .breadcrumb-item a { color: var(--sf); text-decoration: none; }
+.anthology-breadcrumb-bar .breadcrumb-item.active { color: var(--ink-muted); }
+.anthology-breadcrumb-bar .breadcrumb-item+.breadcrumb-item::before { color: var(--ink-muted); }
+
+/* Hero */
+.anthology-hero {
+    background: linear-gradient(135deg, var(--ink) 0%, #2d2010 100%);
+    padding: 2.5rem 0;
+}
+.hero-inner { display: flex; gap: 2.25rem; align-items: flex-start; }
+
+/* Book cover */
+.book-cover-3d {
+    width: 155px;
+    min-width: 155px;
+    height: 215px;
+    border-radius: 3px 9px 9px 3px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 1.25rem .9rem;
+    position: relative;
+    overflow: hidden;
+    box-shadow: -4px 0 0 rgba(0,0,0,.3), -6px 4px 14px rgba(0,0,0,.4), 4px 4px 18px rgba(0,0,0,.3);
+    flex-shrink: 0;
+}
+.book-cover-3d img {
+    position: absolute; inset: 0;
+    width: 100%; height: 100%;
+    object-fit: cover;
+}
+.book-cover-3d::before {
+    content: '';
+    position: absolute;
+    left: 0; top: 0; bottom: 0;
+    width: 13px;
+    background: linear-gradient(to right, rgba(0,0,0,.4), rgba(0,0,0,.1));
+    border-radius: 3px 0 0 3px;
+    z-index: 2;
+}
+.book-cover-3d::after {
+    content: '';
+    position: absolute; inset: 0;
+    background: repeating-linear-gradient(45deg, transparent, transparent 8px, rgba(255,255,255,.015) 8px, rgba(255,255,255,.015) 9px);
+    z-index: 1;
+}
+.book-text-wrap { position: relative; z-index: 3; text-align: center; }
+.book-title-text {
+    font-family: 'Noto Serif SC', serif;
+    font-size: 1.05rem;
+    font-weight: 600;
+    color: #fff;
+    line-height: 1.65;
+    letter-spacing: .13em;
+    word-break: break-all;
+}
+.book-divider { width: 32px; height: 1px; background: var(--sf); margin: .65rem auto; }
+.book-sub-text { font-size: .65rem; color: rgba(255,255,255,.5); letter-spacing: .06em; line-height: 1.5; }
+
+/* Hero right */
+.hero-content { flex: 1; min-width: 0; }
+.hero-title {
+    font-family: 'Noto Serif SC', serif;
+    font-size: 1.75rem;
+    font-weight: 700;
+    color: #fff;
+    line-height: 1.3;
+    margin-bottom: .4rem;
+}
+.hero-subtitle { font-size: .88rem; color: rgba(255,255,255,.45); font-style: italic; letter-spacing: .04em; margin-bottom: 1.1rem; }
+.hero-tags { display: flex; flex-wrap: wrap; gap: .35rem; margin-bottom: 1.3rem; }
+.hero-tag {
+    font-size: .72rem; padding: 2px 9px; border-radius: 20px;
+    background: rgba(200,134,10,.2); color: var(--sf);
+    border: 1px solid rgba(200,134,10,.3);
+}
+.hero-info-row { display: flex; flex-wrap: wrap; gap: 1.4rem; margin-bottom: 1.3rem; }
+.hi-item { display: flex; align-items: center; gap: .45rem; }
+.hi-label { font-size: .72rem; color: rgba(255,255,255,.4); letter-spacing: .04em; display: block; }
+.hi-value { font-size: .83rem; color: rgba(255,255,255,.82); display: block; }
+.hi-avatar {
+    width: 26px; height: 26px; border-radius: 50%;
+    display: flex; align-items: center; justify-content: center;
+    font-size: .68rem; font-weight: 700; flex-shrink: 0;
+}
+.hero-desc { font-size: .85rem; color: rgba(255,255,255,.6); line-height: 1.85; margin-bottom: 1.6rem; max-width: 600px; }
+.btn-read-primary {
+    background: var(--sf); color: var(--ink);
+    font-weight: 700; font-size: .88rem;
+    padding: .55rem 1.6rem; border-radius: 6px; border: none;
+    cursor: pointer; text-decoration: none;
+    display: inline-flex; align-items: center; gap: .45rem;
+    transition: background .2s, transform .15s;
+}
+.btn-read-primary:hover { background: #dea020; color: var(--ink); transform: translateY(-1px); }
+.btn-outline-hero {
+    background: transparent; color: rgba(255,255,255,.7);
+    font-size: .85rem; padding: .5rem 1.3rem;
+    border-radius: 6px; border: 1px solid rgba(255,255,255,.2);
+    cursor: pointer; text-decoration: none;
+    display: inline-flex; align-items: center; gap: .4rem;
+    transition: all .2s; margin-left: .65rem;
+}
+.btn-outline-hero:hover { border-color: rgba(255,255,255,.5); color: #fff; }
+
+/* Section card */
+.sec-card { background: var(--card-bg); border: 1px solid var(--bdr); border-radius: 10px; overflow: hidden; margin-bottom: 1.3rem; }
+.sec-header {
+    padding: .85rem 1.4rem; border-bottom: 1px solid var(--bdr);
+    display: flex; align-items: center; gap: .55rem;
+}
+.sec-bar { width: 3px; height: 15px; background: var(--sf); border-radius: 2px; flex-shrink: 0; }
+.sec-title { font-family: 'Noto Serif SC', serif; font-size: .9rem; font-weight: 600; color: var(--ink-soft); letter-spacing: .04em; }
+.sec-count {
+    margin-left: auto; font-size: .75rem; color: var(--ink-muted);
+    background: var(--sf-light); padding: 2px 8px; border-radius: 10px;
+}
+
+/* About */
+.sec-body { padding: 1.15rem 1.4rem; font-size: .855rem; color: var(--ink-soft); line-height: 1.95; }
+.sec-body p { margin-bottom: .8rem; }
+.sec-body p:last-child { margin-bottom: 0; }
+
+/* TOC */
+.toc-ul { list-style: none; padding: .35rem 0; margin: 0; }
+.toc-ul li a {
+    display: flex; align-items: center;
+    padding: .65rem 1.4rem;
+    text-decoration: none;
+    border-bottom: 1px solid rgba(232,221,208,.5);
+    transition: background .15s;
+}
+.toc-ul li:last-child a { border-bottom: none; }
+.toc-ul li a:hover { background: var(--sf-pale); }
+.toc-num { font-size: .72rem; color: var(--ink-muted); width: 26px; flex-shrink: 0; }
+.toc-name { font-size: .855rem; color: var(--ink-soft); flex: 1; line-height: 1.4; }
+.toc-ul li a:hover .toc-name { color: var(--sf); }
+.toc-arrow { color: var(--bdr); font-size: .85rem; }
+.toc-ul li a:hover .toc-arrow { color: var(--sf); }
+
+/* Sidebar */
+.sb-card { background: var(--card-bg); border: 1px solid var(--bdr); border-radius: 10px; overflow: hidden; margin-bottom: 1.15rem; }
+.sb-head {
+    padding: .8rem 1.2rem; border-bottom: 1px solid var(--bdr);
+    font-family: 'Noto Serif SC', serif; font-size: .875rem;
+    font-weight: 600; color: var(--ink-soft); letter-spacing: .04em;
+    display: flex; align-items: center; gap: .45rem;
+}
+.sb-head::before { content: ''; display: block; width: 3px; height: 13px; background: var(--sf); border-radius: 2px; }
+.smeta-row { display: flex; padding: .7rem 1.2rem; border-bottom: 1px solid var(--bdr); font-size: .8rem; align-items: flex-start; gap: .45rem; }
+.smeta-row:last-child { border-bottom: none; }
+.smeta-label { color: var(--ink-muted); min-width: 65px; flex-shrink: 0; }
+.smeta-value { color: var(--ink-soft); font-weight: 500; }
+.smeta-value a { color: var(--sf); text-decoration: none; }
+.smeta-value a:hover { text-decoration: underline; }
+
+/* Author card */
+.author-block { display: flex; align-items: center; gap: .8rem; padding: 1.1rem 1.2rem; }
+.author-av-lg {
+    width: 48px; height: 48px; border-radius: 50%;
+    display: flex; align-items: center; justify-content: center;
+    font-size: .95rem; font-weight: 700; flex-shrink: 0;
+}
+.author-av-img {
+    width: 48px; height: 48px; border-radius: 50%;
+    object-fit: cover; flex-shrink: 0;
+}
+.hi-avatar {
+    width: 26px; height: 26px; border-radius: 50%;
+    display: flex; align-items: center; justify-content: center;
+    font-size: .68rem; font-weight: 700; flex-shrink: 0;
+}
+.hi-avatar-img {
+    width: 26px; height: 26px; border-radius: 50%;
+    object-fit: cover; flex-shrink: 0;
+}
+.author-block-name { font-weight: 600; font-size: .9rem; color: var(--ink); margin-bottom: .18rem; }
+.author-block-stats { font-size: .75rem; color: var(--ink-muted); }
+.author-bio { font-size: .78rem; color: var(--ink-muted); line-height: 1.65; padding: 0 1.2rem 1.1rem; border-top: 1px solid var(--bdr); padding-top: .9rem; }
+
+/* Related */
+.related-ul { list-style: none; padding: 0; margin: 0; }
+.related-ul li a {
+    display: flex; align-items: center; gap: .7rem;
+    padding: .7rem 1.2rem; border-bottom: 1px solid var(--bdr);
+    text-decoration: none; transition: background .15s;
+}
+.related-ul li:last-child a { border-bottom: none; }
+.related-ul li a:hover { background: var(--sf-pale); }
+.related-cover-mini {
+    width: 34px; height: 46px; border-radius: 2px 5px 5px 2px;
+    display: flex; align-items: center; justify-content: center;
+    font-size: .6rem; color: rgba(255,255,255,.8);
+    font-family: 'Noto Serif SC', serif;
+    flex-shrink: 0; text-align: center; line-height: 1.3;
+}
+.related-t { font-size: .8rem; color: var(--ink-soft); font-weight: 500; margin-bottom: .18rem; line-height: 1.3; }
+.related-ul li a:hover .related-t { color: var(--sf); }
+.related-a { font-size: .7rem; color: var(--ink-muted); }
+
+@media (max-width: 900px) {
+    .hero-inner { flex-direction: column; align-items: center; }
+    .book-cover-3d { height: 170px; }
+}
+</style>
+@endpush
+
+@section('content')
+
+{{-- Breadcrumb --}}
+<div class="anthology-breadcrumb-bar">
+    <div class="container-xl">
+        <ol class="breadcrumb">
+            <li class="breadcrumb-item"><a href="{{ route('library.home') }}">首页</a></li>
+            <li class="breadcrumb-item"><a href="{{ route('library.anthology.index') }}">文集</a></li>
+            <li class="breadcrumb-item active">{{ $anthology['title'] }}</li>
+        </ol>
+    </div>
+</div>
+
+{{-- Hero --}}
+<div class="anthology-hero">
+    <div class="container-xl">
+        <div class="hero-inner">
+
+            {{-- 3D Book Cover --}}
+            <div class="book-cover-3d" style="{{ $anthology['cover_image'] ? '' : 'background: ' . $anthology['cover_gradient'] }}">
+                @if($anthology['cover_image'])
+                    <img src="{{ $anthology['cover_image'] }}" alt="{{ $anthology['title'] }}">
+                @else
+                    <div class="book-text-wrap">
+                        <div class="book-title-text">{{ $anthology['title'] }}</div>
+                        <div class="book-divider"></div>
+                        <div class="book-sub-text">{{ $anthology['subtitle'] ?? '' }}</div>
+                    </div>
+                @endif
+            </div>
+
+            {{-- Content --}}
+            <div class="hero-content">
+                <div class="hero-title">{{ $anthology['title'] }}</div>
+                @if(!empty($anthology['subtitle']))
+                <div class="hero-subtitle">{{ $anthology['subtitle'] }}</div>
+                @endif
+
+                @if(!empty($anthology['tags']))
+                <div class="hero-tags">
+                    @foreach($anthology['tags'] as $tag)
+                    <span class="hero-tag">{{ $tag }}</span>
+                    @endforeach
+                </div>
+                @endif
+
+                <div class="hero-info-row">
+                    <div class="hi-item">
+                        @if(!empty($anthology['author']['avatar']))
+                            <img src="{{ $anthology['author']['avatar'] }}" class="hi-avatar-img" alt="">
+                        @else
+                            <div class="hi-avatar" style="background: {{ $anthology['author']['color'] }}; color: #fff">
+                                {{ $anthology['author']['initials'] }}
+                            </div>
+                        @endif
+                        <div>
+                            <span class="hi-label">作者</span>
+                            <span class="hi-value">{{ $anthology['author']['name'] }}</span>
+                        </div>
+                    </div>
+                    <div class="hi-item">
+                        <div>
+                            <span class="hi-label">最后更新</span>
+                            <span class="hi-value">{{ $anthology['updated_at'] }}</span>
+                        </div>
+                    </div>
+                    <div class="hi-item">
+                        <div>
+                            <span class="hi-label">章节数</span>
+                            <span class="hi-value">{{ $anthology['children_number'] }} 章节</span>
+                        </div>
+                    </div>
+                    <div class="hi-item">
+                        <div>
+                            <span class="hi-label">创建时间</span>
+                            <span class="hi-value">{{ $anthology['created_at'] }}</span>
+                        </div>
+                    </div>
+                </div>
+
+                @if(!empty($anthology['description']))
+                <div class="hero-desc">{{ $anthology['description'] }}</div>
+                @endif
+
+                <div>
+                    @if(!empty($anthology['articles']))
+                    <a href="{{ route('library.anthology.read', ['anthology' => $anthology['id'], 'article' => $anthology['articles'][0]['id']]) }}" class="btn-read-primary">
+                        <i class="ti ti-book-2"></i>
+                        在线阅读
+                    </a>
+                    @endif
+                    <a href="{{ config('mint.server.dashboard_base_path') }}/workspace/anthology/{{ $anthology['id'] }}" class="btn-outline-hero">
+                        <i class="ti ti-pencil"></i>
+                        在编辑器中打开
+                    </a>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>
+
+{{-- Body --}}
+<div class="page-body" style="background: var(--sf-pale);">
+    <div class="container-xl">
+        <div class="row mt-2">
+
+            {{-- Left --}}
+            <div class="col-lg-8">
+
+                {{-- About --}}
+                @if(!empty($anthology['about']))
+                <div class="sec-card">
+                    <div class="sec-header">
+                        <div class="sec-bar"></div>
+                        <div class="sec-title">关于本文集</div>
+                    </div>
+                    <div class="sec-body">
+                        @foreach(explode("\n", $anthology['about']) as $para)
+                            @if(trim($para))
+                            <p>{{ trim($para) }}</p>
+                            @endif
+                        @endforeach
+                    </div>
+                </div>
+                @endif
+
+                {{-- TOC --}}
+                <div class="sec-card">
+                    <div class="sec-header">
+                        <div class="sec-bar"></div>
+                        <div class="sec-title">目录</div>
+                        <div class="sec-count">{{ $anthology['children_number'] }} 章节</div>
+                    </div>
+                    <ul class="toc-ul">
+                        @foreach($anthology['articles'] as $article)
+                        <li>
+                            <a href="{{ route('library.anthology.read', ['anthology' => $anthology['id'], 'article' => $article['id']]) }}">
+                                <span class="toc-num">{{ str_pad($article['order'], 2, '0', STR_PAD_LEFT) }}</span>
+                                <span class="toc-name">{{ $article['title'] }}</span>
+                                <span class="toc-arrow">›</span>
+                            </a>
+                        </li>
+                        @endforeach
+                    </ul>
+                </div>
+
+            </div>{{-- /col --}}
+
+            {{-- Sidebar --}}
+            <div class="col-lg-4">
+
+                {{-- Meta --}}
+                <div class="sb-card">
+                    <div class="sb-head">文集信息</div>
+                    <div class="smeta-row">
+                        <span class="smeta-label">作者</span>
+                        <span class="smeta-value"><a href="#">{{ $anthology['author']['name'] }}</a></span>
+                    </div>
+                    @if(!empty($anthology['language']))
+                    <div class="smeta-row">
+                        <span class="smeta-label">语言</span>
+                        <span class="smeta-value">{{ $anthology['language'] }}</span>
+                    </div>
+                    @endif
+                    <div class="smeta-row">
+                        <span class="smeta-label">章节</span>
+                        <span class="smeta-value">{{ $anthology['children_number'] }} 章节</span>
+                    </div>
+                    <div class="smeta-row">
+                        <span class="smeta-label">创建</span>
+                        <span class="smeta-value">{{ $anthology['created_at'] }}</span>
+                    </div>
+                    <div class="smeta-row">
+                        <span class="smeta-label">更新</span>
+                        <span class="smeta-value">{{ $anthology['updated_at'] }}</span>
+                    </div>
+                    @if(!empty($anthology['category']))
+                    <div class="smeta-row">
+                        <span class="smeta-label">分类</span>
+                        <span class="smeta-value">{{ $anthology['category'] }}</span>
+                    </div>
+                    @endif
+                </div>
+
+                {{-- Author --}}
+                <div class="sb-card">
+                    <div class="sb-head">作者</div>
+                    <div class="author-block">
+                        @if(!empty($anthology['author']['avatar']))
+                            <img src="{{ $anthology['author']['avatar'] }}" class="author-av-img" alt="">
+                        @else
+                            <div class="author-av-lg" style="background: {{ $anthology['author']['color'] }}; color: #fff">
+                                {{ $anthology['author']['initials'] }}
+                            </div>
+                        @endif
+                        <div>
+                            <div class="author-block-name">{{ $anthology['author']['name'] }}</div>
+                            <div class="author-block-stats">
+                                @if($anthology['author']['article_count'])
+                                    {{ $anthology['author']['article_count'] }} 篇文章
+                                @endif
+                            </div>
+                        </div>
+                    </div>
+                    @if(!empty($anthology['author']['bio']))
+                    <div class="author-bio">{{ $anthology['author']['bio'] }}</div>
+                    @endif
+                </div>
+
+                {{-- Related --}}
+                @if($related->count())
+                <div class="sb-card">
+                    <div class="sb-head">相关文集</div>
+                    <ul class="related-ul">
+                        @foreach($related as $rel)
+                        <li>
+                            <a href="{{ route('library.anthology.show', $rel['id']) }}">
+                                <div class="related-cover-mini" style="background: {{ $rel['cover_gradient'] }}">
+                                    {{ mb_substr($rel['title'], 0, 4) }}
+                                </div>
+                                <div>
+                                    <div class="related-t">{{ $rel['title'] }}</div>
+                                    <div class="related-a">{{ $rel['author_name'] }}</div>
+                                </div>
+                            </a>
+                        </li>
+                        @endforeach
+                    </ul>
+                </div>
+                @endif
+
+            </div>
+
+        </div>
+    </div>
+</div>
+@endsection