- 任命/撤销事件增加 type 字段区分类型 - 任命:全屏礼花 + 紫色弹窗 + 紫色系统消息 - 撤销:灰色弹窗 + 灰色系统消息,无礼花 - 消息分发:操作者/被操作者显示在私聊面板,其他人显示在公屏 - 系统消息加随机鼓励语(各5条轮换) - ChatStateService 修复 Redis key 前缀扫描问题(getAllActiveRoomIds) - 用户名片折叠优化:管理员视野、职务履历均可折叠 - 管理操作 + 职务操作合并为「🔧 管理操作」折叠区 - 悄悄话改为「🎁 送礼物」按钮,礼物面板内联展开
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
|