From b7ad3fda49ae11c3636ad6f113025696ddd2013d Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Wed, 29 Jul 2026 09:10:18 +0800 Subject: [PATCH] =?UTF-8?q?=E7=9B=B4=E6=92=AD=E6=97=A5=E5=8E=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 1 + app/Http/Controllers/CalendarController.php | 70 +++++++++++++++++ app/Util/VideoCalendarUtil.php | 48 ++++++++++++ resources/views/calendar/index.blade.php | 86 +++++++++++++++++++++ resources/views/common/header.blade.php | 2 + routes/web.php | 1 + 6 files changed, 208 insertions(+) create mode 100644 app/Http/Controllers/CalendarController.php create mode 100644 app/Util/VideoCalendarUtil.php create mode 100644 resources/views/calendar/index.blade.php diff --git a/CLAUDE.md b/CLAUDE.md index f9a7a32..8df8457 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,6 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - 视频评论查询和搜索 - 节目管理和视频关联 - 弹幕数据管理(支持B站、西瓜视频、抖音) +- 直播日历(/calendar,按稿件标题中的直播日期展示月历) - WebAuthn 身份验证 - 文件上传和管理 diff --git a/app/Http/Controllers/CalendarController.php b/app/Http/Controllers/CalendarController.php new file mode 100644 index 0000000..24571c0 --- /dev/null +++ b/app/Http/Controllers/CalendarController.php @@ -0,0 +1,70 @@ +get("month", ""); + if (preg_match("/^(\d{4})-(0[1-9]|1[0-2])$/", $month_param, $matches)) { + $month = Carbon::createFromDate((int)$matches[1], (int)$matches[2], 1)->startOfMonth(); + } else { + $month = Carbon::now()->startOfMonth(); + } + + $videos = Videos::query()->orderBy("created_at")->get(["id", "bvid", "title", "total_duration"]); + $by_date = VideoCalendarUtil::group_by_live_date($videos); + $parsed_count = array_sum(array_map("sizeof", $by_date)); + + $weeks = []; + $current_week = array_fill(0, 7, null); + $cursor = $month->copy(); + $month_end = $month->copy()->endOfMonth(); + while ($cursor->lte($month_end)) { + $day_index = $cursor->dayOfWeekIso - 1; // 周一为一周之始 + $date_key = $cursor->format("Y-m-d"); + $current_week[$day_index] = [ + "day" => $cursor->day, + "videos" => $by_date[$date_key] ?? [], + "is_today" => $cursor->isToday(), + ]; + if ($day_index === 6) { + $weeks[] = $current_week; + $current_week = array_fill(0, 7, null); + } + $cursor->addDay(); + } + if (sizeof(array_filter($current_week)) > 0) { + $weeks[] = $current_week; + } + + $month_prefix = $month->format("Y-m"); + $live_days = sizeof(array_filter(array_keys($by_date), function (string $date) use ($month_prefix) { + return strpos($date, $month_prefix) === 0; + })); + + $min_month = sizeof($by_date) > 0 ? Carbon::createFromFormat("Y-m-d", array_key_first($by_date))->startOfMonth() : $month->copy(); + $max_month = sizeof($by_date) > 0 ? Carbon::createFromFormat("Y-m-d", array_key_last($by_date))->startOfMonth() : $month->copy(); + $prev_month = $month->copy()->subMonth(); + $next_month = $month->copy()->addMonth(); + + return view("calendar.index", [ + "weeks" => $weeks, + "month" => $month, + "is_current_month" => $month->eq(Carbon::now()->startOfMonth()), + "live_days" => $live_days, + "unparsed_count" => sizeof($videos) - $parsed_count, + "prev_month" => $prev_month, + "next_month" => $next_month, + "has_prev" => $prev_month->gte($min_month), + "has_next" => $next_month->lte($max_month), + ]); + } +} diff --git a/app/Util/VideoCalendarUtil.php b/app/Util/VideoCalendarUtil.php new file mode 100644 index 0000000..ba9745d --- /dev/null +++ b/app/Util/VideoCalendarUtil.php @@ -0,0 +1,48 @@ +startOfDay(); + } + + /** + * 按直播日期分组稿件,返回 ['Y-m-d' => [video, ...]],按日期升序。 + * 标题无法解析的稿件不会进入结果。 + */ + public static function group_by_live_date(iterable $videos): array + { + $grouped = []; + foreach ($videos as $video) { + $date = static::parse_live_date($video->title ?? null); + if ($date === null) { + continue; + } + $grouped[$date->format("Y-m-d")][] = $video; + } + ksort($grouped); + return $grouped; + } +} diff --git a/resources/views/calendar/index.blade.php b/resources/views/calendar/index.blade.php new file mode 100644 index 0000000..d154360 --- /dev/null +++ b/resources/views/calendar/index.blade.php @@ -0,0 +1,86 @@ + + + + 直播日历 - 录播节目查询 + + + + +@include("common.header") + +
+
+ +
+ @if($has_prev) + + ‹ {{$prev_month->format("Y年n月")}} + + @else + + ‹ {{$prev_month->format("Y年n月")}} + + @endif +
+

{{$month->format("Y年n月")}}

+ @if(!$is_current_month) + 回到本月 + @endif +
+ @if($has_next) + + {{$next_month->format("Y年n月")}} › + + @else + + {{$next_month->format("Y年n月")}} › + + @endif +
+ + +
+
+ @foreach(["一", "二", "三", "四", "五", "六", "日"] as $weekday) +
周{{$weekday}}
+ @endforeach +
+
+ @foreach($weeks as $week) + @foreach($week as $cell) + @if($cell === null) +
+ @else +
+
+ @if($cell["is_today"]) + {{$cell["day"]}} + @else + 0 ? "font-bold text-gray-900 dark:text-white" : "text-gray-400 dark:text-gray-500"}}">{{$cell["day"]}} + @endif +
+ @foreach($cell["videos"] as $video) + + {{$video->total_duration ?? "录播"}} + + @endforeach +
+ @endif + @endforeach + @endforeach +
+
+ + +
+ 本月直播 {{$live_days}} 天 + @if($unparsed_count > 0) + · + 另有 {{$unparsed_count}} 条稿件标题未包含可解析日期(剪辑/合集等),未纳入日历 + @endif +
+
+
+@include("common.footer") + + diff --git a/resources/views/common/header.blade.php b/resources/views/common/header.blade.php index 865f884..3eb6a9c 100644 --- a/resources/views/common/header.blade.php +++ b/resources/views/common/header.blade.php @@ -25,6 +25,7 @@ 节目单查询 稿件查询 + 直播日历 节目查询 @auth("web") 建设 @@ -63,6 +64,7 @@
节目单查询 稿件查询 + 直播日历 节目查询 @auth("web") 节目建设 diff --git a/routes/web.php b/routes/web.php index ca9ee62..1983f1b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -19,6 +19,7 @@ Route::get('/comments', ["\\App\\Http\\Controllers\\CommentQueryController","ind Route::get('/video/{video}', ["\\App\\Http\\Controllers\\VideoQueryController","info"]); Route::get('/programs', ["\\App\\Http\\Controllers\\ProgramQueryController","index"]); Route::get('/programs/{program}/video', ["\\App\\Http\\Controllers\\ProgramQueryController","videos"]); +Route::get('/calendar', ["\\App\\Http\\Controllers\\CalendarController","index"]); Route::get('/danmakus', ["\\App\\Http\\Controllers\\DanmakuQueryController","index"]); Route::get('/danmakus/{bvid}', ["\\App\\Http\\Controllers\\DanmakuQueryController","specific_search"]); Route::get('/upload', ["\\App\\Http\\Controllers\\FileController","index"]);