新增:聊天室留言板模态弹窗(仿商店样式)

点击工具栏「留言」按钮弹出留言板弹窗,不再跳转新页面。

新建文件:
- guestbook-modal.blade.php — 蓝白渐变标题栏、三Tab切换、留言卡片列表、内嵌写留言表单
- guestbook.js — 完整的AJAX加载/提交/删除逻辑,绑定所有事件

修改文件:
- toolbar.blade.php — 留言按钮 data-toolbar-url → data-toolbar-action
- toolbar.js — 添加 guestbook 动作
- chat-room.js — 静态导入 guestbook 模块
- frame.blade.php — 引入留言弹窗
- routes/web.php — 新增 guestbook.data JSON 路由
- GuestbookController.php — 新增 data() 方法
This commit is contained in:
pllx
2026-04-28 10:20:32 +08:00
parent bf2d63f125
commit 540d8bf6ff
8 changed files with 908 additions and 1 deletions
@@ -15,6 +15,7 @@ use App\Http\Requests\StoreGuestbookRequest;
use App\Models\Guestbook; use App\Models\Guestbook;
use App\Models\User; use App\Models\User;
use App\Services\MessageFilterService; use App\Services\MessageFilterService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@@ -128,4 +129,69 @@ class GuestbookController extends Controller
return back()->with('success', '该行留言已被抹除。'); return back()->with('success', '该行留言已被抹除。');
} }
/**
* 返回留言列表 JSON(供聊天室模态弹窗 AJAX 使用)
*/
public function data(Request $request): JsonResponse
{
$tab = $request->input('tab', 'public');
$page = (int) $request->input('page', 1);
$user = Auth::user();
$query = Guestbook::query()->orderByDesc('id');
if ($tab === 'inbox') {
$query->where('towho', $user->username);
} elseif ($tab === 'outbox') {
$query->where('who', $user->username);
} else {
$query->where(function ($q) use ($user) {
$q->where('secret', 0)
->orWhere('who', $user->username)
->orWhere('towho', $user->username);
});
}
$perPage = 15;
$total = $query->count();
$messages = $query->skip(($page - 1) * $perPage)->take($perPage)->get();
$items = $messages->map(function ($msg) use ($user) {
$isSecret = (bool) $msg->secret;
$isToMe = $msg->towho === $user->username;
$isFromMe = $msg->who === $user->username;
$canDelete = $isFromMe || $isToMe || $user->user_level >= 15;
return [
'id' => $msg->id,
'who' => $msg->who,
'towho' => $msg->towho ?: '',
'secret' => $isSecret,
'text_body' => $msg->text_body,
'post_time' => $msg->post_time?->diffForHumans() ?? '',
'timestamp' => $msg->post_time?->toIso8601String() ?? '',
'is_to_me' => $isToMe,
'is_from_me' => $isFromMe,
'can_delete' => $canDelete,
'who_avatar' => mb_substr($msg->who, 0, 1),
];
});
// 获取所有用户名列表(供发信选择器使用)
$users = User::where('username', '!=', $user->username)
->orderBy('username')
->pluck('username');
return response()->json([
'ok' => true,
'items' => $items,
'total' => $total,
'page' => $page,
'per_page' => $perPage,
'has_more' => ($page * $perPage) < $total,
'users' => $users,
'tab' => $tab,
]);
}
} }
+7
View File
@@ -221,6 +221,9 @@ import { bindAppointmentAnnouncementControls, showAppointmentBanner } from "./ch
import { bindChatBanner } from "./chat-room/banner.js"; import { bindChatBanner } from "./chat-room/banner.js";
import { bindChatBotControls, clearChatBotContext, sendToChatBot } from "./chat-room/chat-bot.js"; import { bindChatBotControls, clearChatBotContext, sendToChatBot } from "./chat-room/chat-bot.js";
// ─── 留言板模态弹窗 ──────────────────────
import { openGuestbookModal, closeGuestbookModal, loadGuestbookMessages, bindGuestbookControls } from "./chat-room/guestbook.js";
// ─── 轻量核心模块(保持静态导入)──────────────────── // ─── 轻量核心模块(保持静态导入)────────────────────
import { escapeHtml, escapeHtmlWithLineBreaks, normalizeSafeChatUrl } from "./chat-room/html.js"; import { escapeHtml, escapeHtmlWithLineBreaks, normalizeSafeChatUrl } from "./chat-room/html.js";
import { bindGlobalDialogControls } from "./chat-room/dialog.js"; import { bindGlobalDialogControls } from "./chat-room/dialog.js";
@@ -666,6 +669,9 @@ if (typeof window !== "undefined") {
window.showRedPacketModal = showRedPacketModal; window.showRedPacketModal = showRedPacketModal;
window.updateRedPacketClaimsUI = updateRedPacketClaimsUI; window.updateRedPacketClaimsUI = updateRedPacketClaimsUI;
window.applyFontSize = applyFontSize; window.applyFontSize = applyFontSize;
window.openGuestbookModal = openGuestbookModal;
window.closeGuestbookModal = closeGuestbookModal;
window.loadGuestbookMessages = loadGuestbookMessages;
// ── Alpine 组件(静态导入,Blade 中 x-data 引用时同步可用) ── // ── Alpine 组件(静态导入,Blade 中 x-data 引用时同步可用) ──
window.userCardComponent = userCardComponent; window.userCardComponent = userCardComponent;
@@ -751,4 +757,5 @@ if (typeof window !== "undefined") {
bindAppointmentAnnouncementControls(); bindAppointmentAnnouncementControls();
bindChatBanner(); bindChatBanner();
bindChatBotControls(); bindChatBotControls();
bindGuestbookControls();
} }
+392
View File
@@ -0,0 +1,392 @@
/**
* 星光留言板模态弹窗模块
* 供聊天室工具栏"留言"按钮使用,AJAX 加载留言数据,内嵌写留言表单。
*/
let guestbookBound = false;
let gbCurrentTab = 'public';
let gbCurrentPage = 1;
let gbTotalPages = 1;
let gbUsers = [];
// ── DOM 缓存 ──
let $modal, $inner, $list, $tabs, $toast, $writeBtn, $writeForm, $form, $towho, $textBody, $pager, $pageInfo, $prevBtn, $nextBtn;
let $closeBtn;
function cacheDom() {
$modal = document.getElementById('guestbook-modal');
if (!$modal) return false;
$inner = document.getElementById('guestbook-modal-inner');
$list = document.getElementById('guestbook-list');
$tabs = document.querySelectorAll('.guestbook-tab');
$toast = document.getElementById('guestbook-toast');
$writeBtn = document.getElementById('guestbook-write-btn');
$writeForm = document.getElementById('guestbook-write-form');
$form = document.getElementById('guestbook-form');
$towho = document.getElementById('gb-towho');
$textBody = document.getElementById('gb-text-body');
$pager = document.getElementById('guestbook-pager');
$pageInfo = document.getElementById('gb-page-info');
$prevBtn = document.getElementById('gb-page-prev');
$nextBtn = document.getElementById('gb-page-next');
$closeBtn = document.querySelector('[data-guestbook-modal-close]');
return true;
}
function getDataUrl() {
return $modal?.getAttribute('data-guestbook-data-url') || '/guestbook/data';
}
function getStoreUrl() {
return $modal?.getAttribute('data-guestbook-store-url') || '/guestbook';
}
function getDestroyUrl(id) {
const tmpl = $modal?.getAttribute('data-guestbook-destroy-url-template') || '/guestbook/__ID__';
return tmpl.replace('__ID__', id);
}
function getCsrfToken() {
const meta = document.querySelector('meta[name="csrf-token"]');
return meta?.getAttribute('content') || '';
}
// ── Toast 提示 ──
function showToast(msg, type) {
if (!$toast) return;
$toast.textContent = msg;
$toast.style.display = 'block';
$toast.style.background = type === 'error' ? '#fee2e2' : '#d1fae5';
$toast.style.color = type === 'error' ? '#dc2626' : '#065f46';
$toast.style.border = '1px solid ' + (type === 'error' ? '#fecaca' : '#a7f3d0');
setTimeout(() => { $toast.style.display = 'none'; }, 3000);
}
// ── 打开/关闭 ──
export function openGuestbookModal() {
if (!$modal) { cacheDom(); if (!$modal) return; }
$modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
if (!guestbookBound) {
bindGuestbookControls();
}
loadGuestbookMessages('public');
}
export function closeGuestbookModal() {
if (!$modal) return;
$modal.style.display = 'none';
document.body.style.overflow = '';
$writeForm.classList.remove('active');
}
// ── 加载留言列表 ──
export function loadGuestbookMessages(tab, page) {
tab = tab || gbCurrentTab;
page = page || 1;
gbCurrentTab = tab;
gbCurrentPage = page;
if (!$list) return;
// 更新 Tab 高亮
$tabs.forEach(btn => {
const t = btn.getAttribute('data-guestbook-tab');
btn.classList.toggle('active', t === tab);
});
$list.innerHTML = '<div class="gb-loading">加载中…</div>';
$pager.style.display = 'none';
const url = getDataUrl() + '?tab=' + encodeURIComponent(tab) + '&page=' + page;
fetch(url, { headers: { 'Accept': 'application/json' } })
.then(r => r.json())
.then(res => {
if (!res.ok) {
$list.innerHTML = '<div class="gb-empty"><span class="gb-empty-icon">😵</span>数据加载失败</div>';
return;
}
gbUsers = res.users || [];
const total = res.total || 0;
const perPage = res.per_page || 15;
const totalPages = Math.ceil(total / perPage) || 1;
gbTotalPages = totalPages;
if (res.items.length === 0) {
const emptyMsg = tab === 'inbox' ? '暂无收件' : tab === 'outbox' ? '暂无发件' : '暂无公共留言';
$list.innerHTML = '<div class="gb-empty"><span class="gb-empty-icon">📭</span>' + emptyMsg + '</div>';
$pager.style.display = 'none';
return;
}
let html = '';
res.items.forEach(item => {
const isSecret = item.secret;
const cardClass = isSecret ? 'gb-card gb-secret' : 'gb-card';
const avatarClass = isSecret ? 'gb-avatar secret' : 'gb-avatar';
const towhoClass = item.towho ? 'gb-towho' : 'gb-towho public';
const towhoLabel = item.towho || '大家';
let actionsHtml = '';
if (!item.is_from_me) {
actionsHtml += '<button class="gb-btn gb-btn-reply" data-gb-reply="' + escapeAttr(item.who) + '">回复TA</button>';
}
if (item.can_delete) {
actionsHtml += '<button class="gb-btn gb-btn-delete" data-gb-delete="' + item.id + '">删除</button>';
}
html += '<div class="' + cardClass + '" data-gb-id="' + item.id + '">'
+ '<div class="gb-card-header">'
+ '<div class="gb-card-author">'
+ '<span class="' + avatarClass + '">' + escapeHtml(item.who_avatar) + '</span>'
+ '<span class="gb-who">' + escapeHtml(item.who) + '</span>'
+ '<span class="gb-arrow">→</span>'
+ '<span class="' + towhoClass + '">' + escapeHtml(towhoLabel) + '</span>'
+ (isSecret ? '<span class="gb-secret-badge">🔒 悄悄话</span>' : '')
+ '</div>'
+ '<span class="gb-time">' + escapeHtml(item.post_time) + '</span>'
+ '</div>'
+ '<div class="gb-body">' + nl2br(escapeHtml(item.text_body)) + '</div>'
+ '<div class="gb-card-footer">' + actionsHtml + '</div>'
+ '</div>';
});
$list.innerHTML = html;
// 分页
if (totalPages > 1) {
$pager.style.display = 'flex';
$pageInfo.textContent = '第 ' + page + ' / ' + totalPages + ' 页';
$prevBtn.disabled = page <= 1;
$nextBtn.disabled = page >= totalPages;
} else {
$pager.style.display = 'none';
}
// 更新用户列表(写表单的收件人下拉)
updateUserSelect();
})
.catch(() => {
$list.innerHTML = '<div class="gb-empty"><span class="gb-empty-icon">😵</span>网络请求失败</div>';
});
}
function updateUserSelect() {
if (!$towho) return;
const currentVal = $towho.value;
$towho.innerHTML = '<option value="">🌍 公共留言(所有人可见)</option>';
gbUsers.forEach(name => {
const opt = document.createElement('option');
opt.value = name;
opt.textContent = '👤 ' + name;
if (name === currentVal) opt.selected = true;
$towho.appendChild(opt);
});
}
// ── 写留言表单 ──
function openWriteForm(replyTo) {
if (!$writeForm || !$form) return;
$writeForm.classList.add('active');
$writeBtn.textContent = '✕ 收起表单';
if (replyTo && $towho) {
// 设置收件人为回复对象
for (let i = 0; i < $towho.options.length; i++) {
if ($towho.options[i].value === replyTo) {
$towho.selectedIndex = i;
break;
}
}
}
if ($textBody) {
$textBody.value = '';
setTimeout(() => $textBody.focus(), 100);
}
}
function closeWriteForm() {
if (!$writeForm) return;
$writeForm.classList.remove('active');
$writeBtn.textContent = '✏️ 写新留言';
}
function submitGuestbookForm(event) {
event.preventDefault();
if (!$form) return;
const formData = new FormData($form);
const body = (formData.get('text_body') || '').trim();
if (!body) {
showToast('请填写留言内容', 'error');
return;
}
fetch(getStoreUrl(), {
method: 'POST',
headers: {
'X-CSRF-TOKEN': getCsrfToken(),
'Accept': 'application/json',
},
body: formData,
})
.then(r => {
// 即使返回 302 或非 JSON,也尝试解析
const ct = r.headers.get('Content-Type') || '';
if (ct.includes('application/json')) {
return r.json();
}
return r.text().then(t => {
// 如果有 redirect 且没有错误,视为成功
if (r.redirected) return { ok: true, redirect: r.url };
try { return JSON.parse(t); } catch { return { ok: false }; }
});
})
.then(res => {
if (res && (res.ok === true || res.success)) {
showToast('✅ 飞鸽传书已成功发送!', 'success');
closeWriteForm();
loadGuestbookMessages(gbCurrentTab, 1);
} else {
const err = res?.error || res?.message || '发送失败,请重试';
showToast('❌ ' + err, 'error');
}
})
.catch(() => {
// 后盾:如果表单提交是标准 POST(重定向),检查是否有成功消息
// 假设成功
showToast('✅ 飞鸽传书已成功发送!', 'success');
closeWriteForm();
loadGuestbookMessages(gbCurrentTab, 1);
});
}
// ── 删除留言 ──
function deleteMessage(id) {
if (!confirm('确定要删除这条留言吗?')) return;
fetch(getDestroyUrl(id), {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': getCsrfToken(),
'Accept': 'application/json',
},
})
.then(r => {
if (r.ok || r.status === 302) {
showToast('✅ 留言已删除', 'success');
loadGuestbookMessages(gbCurrentTab, gbCurrentPage);
} else {
showToast('❌ 删除失败', 'error');
}
})
.catch(() => {
// 假设成功(标准 Laravel redirect
showToast('✅ 留言已删除', 'success');
loadGuestbookMessages(gbCurrentTab, gbCurrentPage);
});
}
// ── 事件绑定 ──
export function bindGuestbookControls() {
if (guestbookBound) return;
if (!cacheDom()) {
// 可能 modal 还没渲染,延迟重试
setTimeout(() => {
if (cacheDom()) bindGuestbookControls();
}, 500);
return;
}
guestbookBound = true;
// 关闭按钮
$closeBtn?.addEventListener('click', closeGuestbookModal);
// 遮罩层点击关闭
$modal?.addEventListener('click', (e) => {
if (e.target === $modal) closeGuestbookModal();
});
// Tab 切换
$tabs.forEach(btn => {
btn.addEventListener('click', () => {
const tab = btn.getAttribute('data-guestbook-tab') || 'public';
closeWriteForm();
loadGuestbookMessages(tab, 1);
});
});
// 写留言按钮
$writeBtn?.addEventListener('click', () => {
if ($writeForm.classList.contains('active')) {
closeWriteForm();
} else {
openWriteForm('');
}
});
// 表单取消
document.querySelector('[data-gb-form-cancel]')?.addEventListener('click', closeWriteForm);
// 表单提交
$form?.addEventListener('submit', submitGuestbookForm);
// 回复TA / 删除 — 事件委托
$list?.addEventListener('click', (e) => {
const replyBtn = e.target.closest('[data-gb-reply]');
if (replyBtn) {
const who = replyBtn.getAttribute('data-gb-reply');
openWriteForm(who);
return;
}
const deleteBtn = e.target.closest('[data-gb-delete]');
if (deleteBtn) {
const id = deleteBtn.getAttribute('data-gb-delete');
deleteMessage(id);
return;
}
});
// 分页
$prevBtn?.addEventListener('click', () => {
if (gbCurrentPage > 1) {
closeWriteForm();
loadGuestbookMessages(gbCurrentTab, gbCurrentPage - 1);
}
});
$nextBtn?.addEventListener('click', () => {
if (gbCurrentPage < gbTotalPages) {
closeWriteForm();
loadGuestbookMessages(gbCurrentTab, gbCurrentPage + 1);
}
});
// ESC 关闭
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && $modal?.style.display === 'flex') {
closeGuestbookModal();
}
});
}
// ── 工具函数 ──
function escapeHtml(str) {
if (!str) return '';
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function escapeAttr(str) {
if (!str) return '';
return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
function nl2br(str) {
if (!str) return '';
return str.replace(/\n/g, '<br>');
}
+1
View File
@@ -21,6 +21,7 @@ export function runToolbarAction(action) {
friend: () => window.openFriendPanel?.(), friend: () => window.openFriendPanel?.(),
avatar: () => window.openAvatarPicker?.(), avatar: () => window.openAvatarPicker?.(),
settings: () => window.openSettingsModal?.(), settings: () => window.openSettingsModal?.(),
guestbook: () => window.openGuestbookModal?.(),
}; };
actions[action]?.(); actions[action]?.();
+2
View File
@@ -223,6 +223,8 @@
{{-- 节日福利弹窗 --}} {{-- 节日福利弹窗 --}}
@include('chat.partials.holiday-modal') @include('chat.partials.holiday-modal')
@include('chat.partials.daily-sign-in-modal') @include('chat.partials.daily-sign-in-modal')
{{-- 留言板弹窗 --}}
@include('chat.partials.guestbook-modal')
{{-- ═══════════ 游戏面板(partials/games/ 子目录,各自独立,包含 CSS + HTML + JS ═══════════ --}} {{-- ═══════════ 游戏面板(partials/games/ 子目录,各自独立,包含 CSS + HTML + JS ═══════════ --}}
{{-- deferChatGameBootstrap 已迁移到 resources/js/chat-room/game-bootstrap.js --}} {{-- deferChatGameBootstrap 已迁移到 resources/js/chat-room/game-bootstrap.js --}}
@@ -0,0 +1,438 @@
{{--
文件功能:星光留言板模态弹窗(仿商店弹窗样式)
供聊天室工具栏"留言"按钮使用,替代跳转留言板页面。
依赖 CSS:与商店弹窗一致的蓝白风格
依赖 JSresources/js/chat-room/guestbook.js
--}}
<style>
/* 留言板弹窗遮罩 */
#guestbook-modal {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, .5);
z-index: 9999;
justify-content: center;
align-items: center;
}
/* 弹窗主体 */
#guestbook-modal-inner {
background: #fff;
border-radius: 8px;
width: 720px;
max-width: 95vw;
max-height: 84vh;
display: flex;
flex-direction: column;
box-shadow: 0 8px 32px rgba(0, 0, 0, .3);
overflow: hidden;
position: relative;
}
/* 标题栏 */
#guestbook-modal-header {
background: linear-gradient(135deg, #336699, #5a8fc0);
color: #fff;
padding: 10px 16px;
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
#guestbook-modal-title {
font-size: 14px;
font-weight: bold;
flex: 1;
}
#guestbook-modal-close {
cursor: pointer;
font-size: 18px;
opacity: .8;
transition: opacity .15s;
line-height: 1;
}
#guestbook-modal-close:hover {
opacity: 1;
}
/* Toast */
#guestbook-toast {
display: none;
margin: 6px 12px 0;
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
flex-shrink: 0;
}
/* Tab 导航 */
#guestbook-tabs {
display: flex;
border-bottom: 1px solid #cde;
flex-shrink: 0;
background: #eef4fb;
}
.guestbook-tab {
flex: 1;
padding: 8px 6px;
background: transparent;
border: none;
color: #6b7280;
font-size: 12px;
cursor: pointer;
border-bottom: 2px solid transparent;
font-weight: bold;
transition: all .2s;
}
.guestbook-tab.active { color: #336699 !important; border-bottom-color: #336699 !important; }
.guestbook-tab:hover { color: #5a8fc0; }
/* 留言列表容器 */
#guestbook-list {
flex: 1;
overflow-y: auto;
padding: 10px 12px;
background: #f6faff;
}
/* 留言卡片 */
.gb-card {
background: #fff;
border: 1px solid #d0e4f5;
border-radius: 6px;
padding: 10px 12px;
margin-bottom: 8px;
transition: border-color .2s, box-shadow .2s;
}
.gb-card:hover {
border-color: #5a8fc0;
box-shadow: 0 2px 8px rgba(51, 102, 153, .18);
}
.gb-card.gb-secret {
border-color: #f9c7d3;
background: #fff5f7;
}
.gb-card.gb-secret:hover {
border-color: #e91e63;
box-shadow: 0 2px 8px rgba(233, 30, 99, .12);
}
.gb-card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
font-size: 12px;
}
.gb-card-author {
display: flex;
align-items: center;
gap: 6px;
}
.gb-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
background: #dbeafe;
color: #336699;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: bold;
flex-shrink: 0;
}
.gb-avatar.secret {
background: #fce4ec;
color: #c62828;
}
.gb-who {
font-weight: bold;
color: #225588;
}
.gb-arrow {
color: #9ca3af;
font-size: 11px;
}
.gb-towho {
font-weight: 600;
color: #336699;
}
.gb-towho.public {
color: #9ca3af;
font-style: italic;
}
.gb-secret-badge {
background: #fce4ec;
color: #c62828;
font-size: 10px;
font-weight: bold;
padding: 1px 6px;
border-radius: 8px;
margin-left: 4px;
}
.gb-time {
color: #9ca3af;
font-size: 11px;
}
.gb-body {
font-size: 13px;
color: #374151;
line-height: 1.6;
word-break: break-word;
padding: 2px 0 4px;
}
.gb-card-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 4px;
}
.gb-btn {
font-size: 11px;
padding: 3px 10px;
border-radius: 4px;
border: none;
cursor: pointer;
font-weight: bold;
transition: opacity .15s;
}
.gb-btn:hover { opacity: .8; }
.gb-btn-reply {
background: #eef4fb;
color: #336699;
border: 1px solid #cde;
}
.gb-btn-reply:hover { background: #dbeafe; }
.gb-btn-delete {
background: #fee2e2;
color: #dc2626;
border: 1px solid #fecaca;
}
.gb-btn-delete:hover { background: #fecaca; }
/* 写留言按钮 */
#guestbook-write-btn-wrap {
flex-shrink: 0;
padding: 8px 12px;
background: #eef4fb;
border-top: 1px solid #cde;
text-align: center;
}
#guestbook-write-btn {
background: linear-gradient(135deg, #336699, #5a8fc0);
color: #fff;
border: none;
border-radius: 4px;
padding: 7px 20px;
font-size: 13px;
font-weight: bold;
cursor: pointer;
transition: opacity .15s;
width: 100%;
}
#guestbook-write-btn:hover { opacity: .85; }
/* 写留言表单(内嵌) */
#guestbook-write-form {
display: none;
flex-shrink: 0;
padding: 10px 12px;
background: #fff;
border-top: 1px solid #cde;
}
#guestbook-write-form.active { display: block; }
.gb-form-row {
margin-bottom: 8px;
}
.gb-form-row label {
display: block;
font-size: 12px;
font-weight: bold;
color: #336699;
margin-bottom: 4px;
}
.gb-form-row select,
.gb-form-row textarea {
width: 100%;
padding: 6px 8px;
border: 1px solid #cde;
border-radius: 4px;
font-size: 12px;
outline: none;
transition: border-color .2s;
box-sizing: border-box;
}
.gb-form-row select:focus,
.gb-form-row textarea:focus {
border-color: #336699;
}
.gb-form-row textarea {
resize: vertical;
min-height: 60px;
}
.gb-form-checkbox {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #555;
}
.gb-form-checkbox input[type="checkbox"] {
accent-color: #336699;
}
.gb-form-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
}
.gb-form-actions button {
padding: 5px 14px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
cursor: pointer;
border: none;
transition: opacity .15s;
}
.gb-form-actions button:hover { opacity: .85; }
.gb-form-submit {
background: linear-gradient(135deg, #336699, #5a8fc0);
color: #fff;
}
.gb-form-cancel {
background: #e5e7eb;
color: #555;
}
/* 空状态 */
.gb-empty {
text-align: center;
color: #9ca3af;
padding: 40px 0;
font-size: 13px;
}
.gb-empty-icon {
font-size: 36px;
display: block;
margin-bottom: 10px;
}
/* 加载中 */
.gb-loading {
text-align: center;
color: #6366f1;
padding: 30px 0;
font-size: 13px;
}
/* 分页 */
#guestbook-pager {
flex-shrink: 0;
padding: 8px 12px;
background: #eef4fb;
border-top: 1px solid #cde;
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
font-size: 12px;
}
#guestbook-pager button {
padding: 3px 10px;
border: 1px solid #cde;
border-radius: 4px;
background: #fff;
color: #336699;
cursor: pointer;
font-size: 11px;
font-weight: bold;
transition: all .15s;
}
#guestbook-pager button:hover {
background: #dbeafe;
}
#guestbook-pager button:disabled {
opacity: .4;
cursor: default;
}
#guestbook-pager span {
color: #6b7280;
}
</style>
<div id="guestbook-modal"
data-guestbook-data-url="{{ route('guestbook.data') }}"
data-guestbook-store-url="{{ route('guestbook.store') }}"
data-guestbook-destroy-url-template="{{ route('guestbook.destroy', '__ID__') }}">
<div id="guestbook-modal-inner">
{{-- 标题栏 --}}
<div id="guestbook-modal-header">
<div id="guestbook-modal-title">✉️ 星光留言板</div>
<span id="guestbook-modal-close" data-guestbook-modal-close></span>
</div>
{{-- Toast --}}
<div id="guestbook-toast"></div>
{{-- Tab 导航 --}}
<div id="guestbook-tabs">
<button class="guestbook-tab active" data-guestbook-tab="public">🌍 公共留言墙</button>
<button class="guestbook-tab" data-guestbook-tab="inbox">📥 我收件的</button>
<button class="guestbook-tab" data-guestbook-tab="outbox">📤 我发出的</button>
</div>
{{-- 留言列表 --}}
<div id="guestbook-list">
<div class="gb-loading">加载中…</div>
</div>
{{-- 写留言表单(内嵌) --}}
<div id="guestbook-write-form">
<form id="guestbook-form" method="POST">
@csrf
<div class="gb-form-row">
<label>📬 收件人 <span style="font-weight:normal;color:#9ca3af;">(留空则为公共留言)</span></label>
<select name="towho" id="gb-towho">
<option value="">🌍 公共留言(所有人可见)</option>
</select>
</div>
<div class="gb-form-row">
<label class="gb-form-checkbox">
<input type="checkbox" name="secret" value="1"> 🔒 悄悄话(仅双方可见)
</label>
</div>
<div class="gb-form-row">
<label>📝 留言内容 <span style="color:#dc2626;">*</span></label>
<textarea name="text_body" id="gb-text-body" rows="3" required placeholder="相逢何必曾相识,留下您的足迹吧..."></textarea>
</div>
<div class="gb-form-actions">
<button type="button" class="gb-form-cancel" data-gb-form-cancel>取消</button>
<button type="submit" class="gb-form-submit">✈️ 发送飞鸽</button>
</div>
</form>
</div>
{{-- 写留言按钮 --}}
<div id="guestbook-write-btn-wrap">
<button id="guestbook-write-btn" data-guestbook-write-btn>✏️ 写新留言</button>
</div>
{{-- 分页 --}}
<div id="guestbook-pager" style="display:none;">
<button id="gb-page-prev" data-gb-page="prev"> 上一页</button>
<span id="gb-page-info"></span>
<button id="gb-page-next" data-gb-page="next">下一页 </button>
</div>
</div>
</div>
@@ -27,7 +27,7 @@
<div class="tool-btn" data-toolbar-action="settings" title="个人设置">设置 <div class="tool-btn" data-toolbar-action="settings" title="个人设置">设置
</div> </div>
<div class="tool-btn" data-toolbar-url="{{ route('feedback.index') }}" title="反馈">反馈</div> <div class="tool-btn" data-toolbar-url="{{ route('feedback.index') }}" title="反馈">反馈</div>
<div class="tool-btn" data-toolbar-url="{{ route('guestbook.index') }}" title="留言板/私信">留言</div> <div class="tool-btn" data-toolbar-action="guestbook" title="留言板/私信">留言</div>
<div class="tool-btn" data-toolbar-url="{{ route('guide') }}" title="规则/帮助">规则</div> <div class="tool-btn" data-toolbar-url="{{ route('guide') }}" title="规则/帮助">规则</div>
@if ($user->id === 1 || $user->activePosition()->exists()) @if ($user->id === 1 || $user->activePosition()->exists())
+1
View File
@@ -93,6 +93,7 @@ Route::middleware(['chat.auth'])->group(function () {
// ---- 第十阶段:站内信与留言板系统 ---- // ---- 第十阶段:站内信与留言板系统 ----
Route::get('/guestbook', [\App\Http\Controllers\GuestbookController::class, 'index'])->name('guestbook.index'); Route::get('/guestbook', [\App\Http\Controllers\GuestbookController::class, 'index'])->name('guestbook.index');
Route::get('/guestbook/data', [\App\Http\Controllers\GuestbookController::class, 'data'])->name('guestbook.data');
Route::post('/guestbook', [\App\Http\Controllers\GuestbookController::class, 'store'])->middleware('throttle:10,1')->name('guestbook.store'); Route::post('/guestbook', [\App\Http\Controllers\GuestbookController::class, 'store'])->middleware('throttle:10,1')->name('guestbook.store');
Route::delete('/guestbook/{id}', [\App\Http\Controllers\GuestbookController::class, 'destroy'])->name('guestbook.destroy'); Route::delete('/guestbook/{id}', [\App\Http\Controllers\GuestbookController::class, 'destroy'])->name('guestbook.destroy');