310 lines
12 KiB
PHP
310 lines
12 KiB
PHP
|
|
{{--
|
|||
|
|
文件功能:开发日志前台独立页面(/changelog)
|
|||
|
|
时间轴样式,懒加载(IntersectionObserver),倒序显示
|
|||
|
|
仅展示已发布的日志(is_published=1)
|
|||
|
|
支持 URL #vYYYY-MM-DD 锚点直跳指定版本
|
|||
|
|
|
|||
|
|
@extends layouts.app
|
|||
|
|
--}}
|
|||
|
|
@extends('layouts.app')
|
|||
|
|
|
|||
|
|
@section('title', '更新日志 - 飘落流星')
|
|||
|
|
@section('nav-icon', '📋')
|
|||
|
|
@section('nav-title', '更新日志')
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
@section('head')
|
|||
|
|
<style>
|
|||
|
|
/* 时间轴主线 */
|
|||
|
|
.tl-line {
|
|||
|
|
position: absolute;
|
|||
|
|
left: 9px;
|
|||
|
|
top: 0;
|
|||
|
|
bottom: 0;
|
|||
|
|
width: 2px;
|
|||
|
|
background: linear-gradient(to bottom, #818cf8 0%, #c4b5fd 70%, transparent 100%);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 时间轴节点圆点 */
|
|||
|
|
.tl-dot {
|
|||
|
|
position: absolute;
|
|||
|
|
left: 0;
|
|||
|
|
top: 22px;
|
|||
|
|
width: 20px;
|
|||
|
|
height: 20px;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
border: 2px solid #818cf8;
|
|||
|
|
background: white;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
font-size: 10px;
|
|||
|
|
transition: transform 0.2s, border-color 0.2s;
|
|||
|
|
z-index: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tl-item:hover .tl-dot {
|
|||
|
|
transform: scale(1.25);
|
|||
|
|
border-color: #4f46e5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Markdown 内容样式 */
|
|||
|
|
.prose-log h1,
|
|||
|
|
.prose-log h2,
|
|||
|
|
.prose-log h3 {
|
|||
|
|
font-weight: 700;
|
|||
|
|
color: #1e1b4b;
|
|||
|
|
margin: 1rem 0 0.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.prose-log h2 {
|
|||
|
|
font-size: 1rem;
|
|||
|
|
border-bottom: 1px solid #e0e7ff;
|
|||
|
|
padding-bottom: 4px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.prose-log ul {
|
|||
|
|
list-style: disc;
|
|||
|
|
padding-left: 1.5rem;
|
|||
|
|
margin: 0.4rem 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.prose-log ol {
|
|||
|
|
list-style: decimal;
|
|||
|
|
padding-left: 1.5rem;
|
|||
|
|
margin: 0.4rem 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.prose-log li {
|
|||
|
|
margin: 0.2rem 0;
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.prose-log p {
|
|||
|
|
margin: 0.4rem 0;
|
|||
|
|
line-height: 1.7;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.prose-log code {
|
|||
|
|
background: #f1f5f9;
|
|||
|
|
padding: 1px 5px;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
font-size: 0.85em;
|
|||
|
|
color: #7c3aed;
|
|||
|
|
font-family: monospace;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.prose-log a {
|
|||
|
|
color: #4f46e5;
|
|||
|
|
text-decoration: underline;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.prose-log strong {
|
|||
|
|
font-weight: 600;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.prose-log blockquote {
|
|||
|
|
border-left: 3px solid #818cf8;
|
|||
|
|
padding-left: 10px;
|
|||
|
|
color: #64748b;
|
|||
|
|
font-style: italic;
|
|||
|
|
margin: 0.5rem 0;
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
@endsection
|
|||
|
|
|
|||
|
|
@section('content')
|
|||
|
|
<div class="max-w-3xl mx-auto py-8 px-4 sm:px-6" x-data="{
|
|||
|
|
items: [],
|
|||
|
|
lastId: null,
|
|||
|
|
hasMore: true,
|
|||
|
|
loading: false,
|
|||
|
|
|
|||
|
|
init() {
|
|||
|
|
// SSR 首屏数据注入
|
|||
|
|
this.items = {{ json_encode(
|
|||
|
|
$changelogs->map(
|
|||
|
|
fn($l) => [
|
|||
|
|
'id' => $l->id,
|
|||
|
|
'version' => $l->version,
|
|||
|
|
'title' => $l->title,
|
|||
|
|
'type_label' => $l->type_label,
|
|||
|
|
'type_color' => $l->type_color,
|
|||
|
|
'content_html' => $l->content_html,
|
|||
|
|
'summary' => $l->summary,
|
|||
|
|
'published_at' => $l->published_at?->format('Y-m-d'),
|
|||
|
|
'expanded' => false,
|
|||
|
|
],
|
|||
|
|
),
|
|||
|
|
) }};
|
|||
|
|
|
|||
|
|
if (this.items.length > 0) {
|
|||
|
|
this.lastId = this.items[this.items.length - 1].id;
|
|||
|
|
this.hasMore = this.items.length >= 10;
|
|||
|
|
} else {
|
|||
|
|
this.hasMore = false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理 URL #v2026-02-28 锚点跳转
|
|||
|
|
if (window.location.hash && window.location.hash.startsWith('#v')) {
|
|||
|
|
const version = window.location.hash.slice(2);
|
|||
|
|
this.$nextTick(() => {
|
|||
|
|
const el = document.getElementById('v' + version);
|
|||
|
|
if (el) {
|
|||
|
|
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|||
|
|
const item = this.items.find(i => i.version === version);
|
|||
|
|
if (item) item.expanded = true;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 懒加载更多
|
|||
|
|
async loadMore() {
|
|||
|
|
if (this.loading || !this.hasMore) return;
|
|||
|
|
this.loading = true;
|
|||
|
|
try {
|
|||
|
|
const res = await fetch(`/changelog/more?after_id=${this.lastId}`);
|
|||
|
|
const data = await res.json();
|
|||
|
|
this.items.push(...data.items.map(i => ({ ...i, expanded: false })));
|
|||
|
|
if (data.items.length > 0) this.lastId = data.items[data.items.length - 1].id;
|
|||
|
|
this.hasMore = data.has_more;
|
|||
|
|
} catch (e) { console.error(e); } finally { this.loading = false; }
|
|||
|
|
},
|
|||
|
|
}">
|
|||
|
|
|
|||
|
|
{{-- 页面标题区 --}}
|
|||
|
|
<div class="flex items-center justify-between mb-8">
|
|||
|
|
<div>
|
|||
|
|
<h2 class="text-2xl font-extrabold text-gray-800 flex items-center gap-2">
|
|||
|
|
📋 <span>开发日志</span>
|
|||
|
|
</h2>
|
|||
|
|
<p class="text-sm text-gray-500 mt-1">记录每次版本的功能新增、Bug 修复与优化改进</p>
|
|||
|
|
</div>
|
|||
|
|
<div class="text-right text-xs text-gray-400">
|
|||
|
|
<p>按更新时间排序</p>
|
|||
|
|
<p class="text-indigo-500 font-medium mt-0.5">共 {{ $changelogs->count() }}+ 条日志</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{{-- 空状态 --}}
|
|||
|
|
<template x-if="items.length === 0">
|
|||
|
|
<div class="text-center py-24 text-gray-400">
|
|||
|
|
<p class="text-6xl mb-4">📭</p>
|
|||
|
|
<p class="text-lg font-bold text-gray-500">暂无更新日志</p>
|
|||
|
|
<p class="text-sm mt-2">开发团队还没有发布任何日志,请稍后再来查看</p>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
{{-- 时间轴 --}}
|
|||
|
|
<div class="relative pl-10">
|
|||
|
|
{{-- 时间轴主线 --}}
|
|||
|
|
<div class="tl-line"></div>
|
|||
|
|
|
|||
|
|
<template x-for="(log, index) in items" :key="log.id">
|
|||
|
|
<div class="tl-item relative mb-8" :id="'v' + log.version">
|
|||
|
|
{{-- 圆点节点 --}}
|
|||
|
|
<div class="tl-dot"
|
|||
|
|
:class="{
|
|||
|
|
'border-emerald-400': log.type_color === 'emerald',
|
|||
|
|
'border-rose-400': log.type_color === 'rose',
|
|||
|
|
'border-blue-400': log.type_color === 'blue',
|
|||
|
|
'border-slate-400': log.type_color === 'slate',
|
|||
|
|
}">
|
|||
|
|
<span
|
|||
|
|
x-text="log.type_color === 'emerald' ? '🆕' : log.type_color === 'rose' ? '🐛' : log.type_color === 'blue' ? '⚡' : '📌'"
|
|||
|
|
class="text-xs"></span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{{-- 日志卡片 --}}
|
|||
|
|
<div
|
|||
|
|
class="bg-white rounded-2xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow duration-200 overflow-hidden">
|
|||
|
|
{{-- 卡片头部 --}}
|
|||
|
|
<div class="px-5 py-4 border-b border-gray-50">
|
|||
|
|
<div class="flex items-center gap-3 flex-wrap">
|
|||
|
|
{{-- 版本号 --}}
|
|||
|
|
<span class="font-mono text-lg font-black text-indigo-700 tracking-wide"
|
|||
|
|
x-text="'v' + log.version"></span>
|
|||
|
|
|
|||
|
|
{{-- 类型标签 --}}
|
|||
|
|
<span class="px-2.5 py-0.5 rounded-full text-xs font-bold"
|
|||
|
|
:class="{
|
|||
|
|
'bg-emerald-100 text-emerald-700': log.type_color === 'emerald',
|
|||
|
|
'bg-rose-100 text-rose-700': log.type_color === 'rose',
|
|||
|
|
'bg-blue-100 text-blue-700': log.type_color === 'blue',
|
|||
|
|
'bg-slate-100 text-slate-600': log.type_color === 'slate',
|
|||
|
|
}"
|
|||
|
|
x-text="log.type_label"></span>
|
|||
|
|
|
|||
|
|
{{-- 发布日期 --}}
|
|||
|
|
<span class="text-gray-400 text-xs ml-auto" x-text="log.published_at"></span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{{-- 标题 --}}
|
|||
|
|
<h3 class="text-base font-bold text-gray-800 mt-2 leading-snug" x-text="log.title"></h3>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{{-- 内容区 --}}
|
|||
|
|
<div class="px-5 py-4">
|
|||
|
|
{{-- 默认折叠:摘要 --}}
|
|||
|
|
<div x-show="!log.expanded">
|
|||
|
|
<p class="text-gray-600 text-sm leading-relaxed" x-text="log.summary"></p>
|
|||
|
|
<button @click="log.expanded = true"
|
|||
|
|
class="mt-3 text-indigo-600 hover:text-indigo-800 text-xs font-semibold hover:underline flex items-center gap-1">
|
|||
|
|
展开完整内容 <span class="text-base leading-none">↓</span>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{{-- 展开:完整 Markdown 内容 --}}
|
|||
|
|
<div x-show="log.expanded" x-transition.opacity.duration.150ms>
|
|||
|
|
<div class="prose-log text-sm text-gray-700" x-html="log.content_html"></div>
|
|||
|
|
<button @click="log.expanded = false"
|
|||
|
|
class="mt-3 text-gray-400 hover:text-gray-600 text-xs font-semibold hover:underline flex items-center gap-1">
|
|||
|
|
收起 <span class="text-base leading-none">↑</span>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{{-- 底部:版本锚点复制链接 --}}
|
|||
|
|
<div class="px-5 py-2 bg-gray-50 border-t border-gray-100 flex items-center justify-between">
|
|||
|
|
<span class="text-gray-400 text-xs">
|
|||
|
|
#<span x-text="log.version"></span>
|
|||
|
|
</span>
|
|||
|
|
<button
|
|||
|
|
@click="navigator.clipboard?.writeText(window.location.origin + '/changelog#v' + log.version).then(() => { $el.textContent = '✅ 已复制'; setTimeout(() => $el.textContent = '🔗 复制链接', 1500) })"
|
|||
|
|
class="text-xs text-gray-400 hover:text-indigo-600 transition">
|
|||
|
|
🔗 复制链接
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
{{-- 懒加载触发哨兵 --}}
|
|||
|
|
<div x-show="hasMore" x-intersect.threshold.10="loadMore()" class="py-6 text-center">
|
|||
|
|
<template x-if="loading">
|
|||
|
|
<div class="flex items-center justify-center gap-2 text-gray-400 text-sm">
|
|||
|
|
<svg class="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
|
|||
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
|
|||
|
|
stroke-width="4"></circle>
|
|||
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z">
|
|||
|
|
</path>
|
|||
|
|
</svg>
|
|||
|
|
加载更多...
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{{-- 全部加载完成提示 --}}
|
|||
|
|
<div x-show="!hasMore && items.length > 0" class="text-center py-6 text-gray-400 text-sm">
|
|||
|
|
<div class="inline-flex items-center gap-2">
|
|||
|
|
<div class="h-px w-16 bg-gray-200"></div>
|
|||
|
|
<span>以上是全部更新日志</span>
|
|||
|
|
<div class="h-px w-16 bg-gray-200"></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
@endsection
|