From 62371a7c6472b4ba41bc977655795c67e374ce89 Mon Sep 17 00:00:00 2001 From: pllx Date: Tue, 28 Apr 2026 10:29:14 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9A=E8=81=8A=E5=A4=A9?= =?UTF-8?q?=E5=AE=A4=E5=8F=8D=E9=A6=88=E6=A8=A1=E6=80=81=E5=BC=B9=E7=AA=97?= =?UTF-8?q?=EF=BC=88=E4=BB=BF=E7=95=99=E8=A8=80=E5=BC=B9=E7=AA=97=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 点击工具栏「反馈」按钮弹出反馈弹窗,不再跳转新页面。 新建文件: - feedback-modal.blade.php — 蓝白渐变标题栏、类型筛选Tabs、反馈卡片列表(展开详情/评论)、提交反馈表单、滚动懒加载 - feedback.js — AJAX加载/提交/点赞/评论/删除,滚动懒加载,乐观UI更新 修改文件: - toolbar.blade.php — 反馈按钮 data-toolbar-url → data-toolbar-action - toolbar.js — 添加 feedback 动作 - chat-room.js — 静态导入 feedback 模块 - frame.blade.php — 引入反馈弹窗 - routes/web.php — 新增 feedback.data 路由 - FeedbackController.php — 新增 data() 方法 --- app/Http/Controllers/FeedbackController.php | 38 ++ resources/js/chat-room.js | 9 + resources/js/chat-room/feedback.js | 625 ++++++++++++++++++ resources/js/chat-room/toolbar.js | 1 + resources/views/chat/frame.blade.php | 2 + .../chat/partials/feedback-modal.blade.php | 577 ++++++++++++++++ .../chat/partials/layout/toolbar.blade.php | 2 +- routes/web.php | 2 + 8 files changed, 1255 insertions(+), 1 deletion(-) create mode 100644 resources/js/chat-room/feedback.js create mode 100644 resources/views/chat/partials/feedback-modal.blade.php diff --git a/app/Http/Controllers/FeedbackController.php b/app/Http/Controllers/FeedbackController.php index df13818..966c08e 100644 --- a/app/Http/Controllers/FeedbackController.php +++ b/app/Http/Controllers/FeedbackController.php @@ -48,6 +48,38 @@ class FeedbackController extends Controller return view('feedback.index', compact('feedbacks', 'myVotedIds')); } + /** + * 获取反馈第一页数据(JSON API) + * 供聊天室模态弹窗使用,格式与 loadMore 一致 + * + * @param Request $request 含 type 筛选参数 + */ + public function data(Request $request): JsonResponse + { + $type = $request->input('type'); // bug|suggestion|null(全部) + + $query = FeedbackItem::with(['replies']) + ->orderByDesc('votes_count') + ->orderByDesc('created_at'); + + if ($type && in_array($type, ['bug', 'suggestion'])) { + $query->ofType($type); + } + + $items = $query->limit(self::PAGE_SIZE)->get(); + + $myVotedIds = FeedbackVote::where('user_id', Auth::id()) + ->whereIn('feedback_id', $items->pluck('id')) + ->pluck('feedback_id') + ->toArray(); + + return response()->json([ + 'items' => $this->formatItems($items, $myVotedIds), + 'last_id' => $items->last()?->id ?? 0, + 'has_more' => $items->count() === self::PAGE_SIZE, + ]); + } + /** * 懒加载更多反馈(JSON API) * 支持按类型筛选(bug / suggestion) @@ -257,6 +289,10 @@ class FeedbackController extends Controller */ private function formatItem(FeedbackItem $item, bool $voted): array { + /** @var \App\Models\User $user */ + $user = Auth::user(); + $isOwner = $item->user_id === $user->id; + return [ 'id' => $item->id, 'type' => $item->type, @@ -272,6 +308,8 @@ class FeedbackController extends Controller 'username' => $item->username, 'created_at' => $item->created_at->diffForHumans(), 'voted' => $voted, + 'is_owner' => $isOwner, + 'can_delete' => ($isOwner && $item->is_within_24_hours) || $user->id === 1, 'replies' => ($item->relationLoaded('replies') ? $item->replies : collect())->map(fn ($r) => [ 'id' => $r->id, 'username' => $r->username, diff --git a/resources/js/chat-room.js b/resources/js/chat-room.js index 316fd20..5f6b184 100644 --- a/resources/js/chat-room.js +++ b/resources/js/chat-room.js @@ -224,6 +224,9 @@ import { bindChatBotControls, clearChatBotContext, sendToChatBot } from "./chat- // ─── 留言板模态弹窗 ────────────────────── import { openGuestbookModal, closeGuestbookModal, loadGuestbookMessages, bindGuestbookControls } from "./chat-room/guestbook.js"; +// ─── 反馈模态弹窗 ────────────────────── +import { openFeedbackModal, closeFeedbackModal, loadFeedbackData, loadMoreFeedback, bindFeedbackControls } from "./chat-room/feedback.js"; + // ─── 轻量核心模块(保持静态导入)──────────────────── import { escapeHtml, escapeHtmlWithLineBreaks, normalizeSafeChatUrl } from "./chat-room/html.js"; import { bindGlobalDialogControls } from "./chat-room/dialog.js"; @@ -672,6 +675,11 @@ if (typeof window !== "undefined") { window.openGuestbookModal = openGuestbookModal; window.closeGuestbookModal = closeGuestbookModal; window.loadGuestbookMessages = loadGuestbookMessages; + window.openFeedbackModal = openFeedbackModal; + window.closeFeedbackModal = closeFeedbackModal; + window.loadFeedbackData = loadFeedbackData; + window.loadMoreFeedback = loadMoreFeedback; + window.bindFeedbackControls = bindFeedbackControls; // ── Alpine 组件(静态导入,Blade 中 x-data 引用时同步可用) ── window.userCardComponent = userCardComponent; @@ -758,4 +766,5 @@ if (typeof window !== "undefined") { bindChatBanner(); bindChatBotControls(); bindGuestbookControls(); + bindFeedbackControls(); } diff --git a/resources/js/chat-room/feedback.js b/resources/js/chat-room/feedback.js new file mode 100644 index 0000000..4e67648 --- /dev/null +++ b/resources/js/chat-room/feedback.js @@ -0,0 +1,625 @@ +/** + * 用户反馈模态弹窗模块 + * 供聊天室工具栏"反馈"按钮使用,AJAX 加载反馈列表,内嵌提交表单。 + */ + +let feedbackBound = false; +let fbCurrentTab = 'all'; +let fbLastId = null; +let fbHasMore = true; +let fbLoading = false; + +// ── DOM 缓存 ── +let $modal, $inner, $list, $tabs, $toast, $writeBtn, $writeOverlay, $form, $type, $title, $content; +let $closeBtn, $writeClose, $formCancel, $loader; + +function cacheDom() { + $modal = document.getElementById('feedback-modal'); + if (!$modal) return false; + $inner = document.getElementById('feedback-modal-inner'); + $list = document.getElementById('feedback-list'); + $tabs = document.querySelectorAll('.feedback-tab'); + $toast = document.getElementById('feedback-toast'); + $writeBtn = document.getElementById('feedback-submit-btn'); + $writeOverlay = document.getElementById('feedback-write-overlay'); + $form = document.getElementById('feedback-form'); + $type = document.getElementById('fb-type'); + $title = document.getElementById('fb-title'); + $content = document.getElementById('fb-content'); + $closeBtn = document.querySelector('[data-feedback-modal-close]'); + $writeClose = document.querySelector('[data-feedback-write-close]'); + $formCancel = document.querySelector('[data-fb-form-cancel]'); + $loader = document.getElementById('feedback-loader'); + return true; +} + +function getDataUrl() { + return $modal?.getAttribute('data-feedback-data-url') || '/feedback/data'; +} + +function getMoreUrl() { + return $modal?.getAttribute('data-feedback-more-url') || '/feedback/more'; +} + +function getStoreUrl() { + return $modal?.getAttribute('data-feedback-store-url') || '/feedback'; +} + +function getVoteUrl(id) { + const tmpl = $modal?.getAttribute('data-feedback-vote-url-template') || '/feedback/__ID__/vote'; + return tmpl.replace('__ID__', id); +} + +function getReplyUrl(id) { + const tmpl = $modal?.getAttribute('data-feedback-reply-url-template') || '/feedback/__ID__/reply'; + return tmpl.replace('__ID__', id); +} + +function getDestroyUrl(id) { + const tmpl = $modal?.getAttribute('data-feedback-destroy-url-template') || '/feedback/__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 openFeedbackModal() { + if (!$modal) { cacheDom(); if (!$modal) return; } + $modal.style.display = 'flex'; + document.body.style.overflow = 'hidden'; + if (!feedbackBound) { + bindFeedbackControls(); + } + loadFeedbackData('all'); +} + +export function closeFeedbackModal() { + if (!$modal) return; + $modal.style.display = 'none'; + document.body.style.overflow = ''; + $writeOverlay?.classList.remove('active'); +} + +// ── 加载反馈数据 ── +export function loadFeedbackData(tab) { + tab = tab || fbCurrentTab; + fbCurrentTab = tab; + fbLastId = null; + fbHasMore = true; + fbLoading = false; + + if (!$list) return; + + // 更新 Tab 高亮 + $tabs.forEach(btn => { + const t = btn.getAttribute('data-feedback-tab'); + btn.classList.toggle('active', t === tab); + }); + + $list.innerHTML = '
加载中…
'; + $loader?.classList.add('hidden'); + + let url = getDataUrl(); + if (tab !== 'all') { + url += '?type=' + encodeURIComponent(tab); + } + + fetch(url, { headers: { 'Accept': 'application/json' } }) + .then(r => r.json()) + .then(res => { + const items = res.items || []; + fbLastId = res.last_id || (items.length > 0 ? items[items.length - 1].id : null); + fbHasMore = res.has_more === true; + + if (items.length === 0) { + $list.innerHTML = '
💬暂无反馈
'; + $loader?.classList.add('hidden'); + return; + } + + renderFeedbackList(items); + updateLoader(); + }) + .catch(() => { + $list.innerHTML = '
😵数据加载失败
'; + $loader?.classList.add('hidden'); + }); +} + +// ── 加载更多 ── +export function loadMoreFeedback() { + if (fbLoading || !fbHasMore || !fbLastId) return; + fbLoading = true; + $loader?.classList.remove('hidden'); + $loader.textContent = '加载中…'; + + let url = getMoreUrl() + '?after_id=' + fbLastId; + if (fbCurrentTab !== 'all') { + url += '&type=' + encodeURIComponent(fbCurrentTab); + } + + fetch(url, { headers: { 'Accept': 'application/json' } }) + .then(r => r.json()) + .then(res => { + const items = res.items || []; + fbLoading = false; + + if (items.length > 0) { + fbLastId = items[items.length - 1].id; + fbHasMore = res.has_more === true; + appendFeedbackList(items); + } else { + fbHasMore = false; + } + + updateLoader(); + }) + .catch(() => { + fbLoading = false; + $loader?.classList.add('hidden'); + }); +} + +// ── 渲染列表 ── +function renderFeedbackList(items) { + let html = ''; + items.forEach(item => { + html += buildFeedbackCard(item); + }); + $list.innerHTML = html; +} + +function appendFeedbackList(items) { + items.forEach(item => { + $list.insertAdjacentHTML('beforeend', buildFeedbackCard(item)); + }); +} + +function buildFeedbackCard(item) { + const typeClass = item.type === 'bug' ? 'bug' : 'suggestion'; + const statusClass = 'fb-status-badge'; + const statusStyle = getStatusStyle(item.status_color); + + let actionsHtml = ''; + // 赞同按钮(不能赞同自己的) + const voteIcon = item.voted ? '👍' : '👆'; + const voteBtnClass = item.voted ? 'fb-vote-btn voted' : 'fb-vote-btn'; + const voteDisabled = item.is_owner ? 'disabled' : ''; + actionsHtml += ``; + + // 删除按钮 + if (item.can_delete) { + actionsHtml += ``; + } + + // 展开评论数 + const repliesLabel = item.replies_count > 0 ? `💬 ${item.replies_count}` : '💬 0'; + + return `
+
+ ${escapeHtml(item.type_label)} + ${escapeHtml(item.status_label)} + ${repliesLabel} +
+
${escapeHtml(item.title)}
+
${escapeHtml(item.username)} · ${escapeHtml(item.created_at)}
+ + + +
`; +} + +function buildReplyHtml(reply) { + const adminClass = reply.is_admin ? 'admin' : ''; + const badgeHtml = reply.is_admin ? '开发者' : ''; + return `
+
+ ${escapeHtml(reply.username)} + ${badgeHtml} + ${escapeHtml(reply.created_at)} +
+
${nl2br(escapeHtml(reply.content))}
+
`; +} + +function getStatusStyle(color) { + const map = { + gray: 'background:#f3f4f6;color:#4b5563;', + green: 'background:#dcfce7;color:#15803d;', + blue: 'background:#dbeafe;color:#1d4ed8;', + emerald: 'background:#d1fae5;color:#047857;', + red: 'background:#fee2e2;color:#dc2626;', + orange: 'background:#ffedd5;color:#c2410c;', + }; + return map[color] || 'background:#f3f4f6;color:#4b5563;'; +} + +function updateLoader() { + if (!$loader) return; + if (fbHasMore) { + $loader.classList.remove('hidden'); + $loader.textContent = fbLoading ? '加载中…' : '↓ 下拉加载更多'; + } else { + $loader.classList.add('hidden'); + } +} + +// ── 提交反馈表单 ── +function openWriteForm() { + if (!$writeOverlay) return; + $writeOverlay.classList.add('active'); + if ($title) { + $title.value = ''; + setTimeout(() => $title.focus(), 100); + } + if ($content) $content.value = ''; + if ($type) $type.value = 'bug'; +} + +function closeWriteForm() { + if (!$writeOverlay) return; + $writeOverlay.classList.remove('active'); +} + +function submitFeedbackForm(event) { + event.preventDefault(); + if (!$form) return; + + const title = ($title?.value || '').trim(); + const content = ($content?.value || '').trim(); + const type = $type?.value || 'bug'; + + if (!title) { + showToast('请填写标题', 'error'); + return; + } + if (!content) { + showToast('请填写详细描述', 'error'); + return; + } + + const submitBtn = $form.querySelector('.fb-form-submit'); + if (submitBtn) { + submitBtn.disabled = true; + submitBtn.textContent = '提交中…'; + } + + fetch(getStoreUrl(), { + method: 'POST', + headers: { + 'X-CSRF-TOKEN': getCsrfToken(), + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify({ type, title, content }), + }) + .then(r => r.json()) + .then(res => { + if (res.status === 'success') { + showToast('✅ ' + (res.message || '反馈已提交,感谢您的贡献!'), 'success'); + closeWriteForm(); + loadFeedbackData(fbCurrentTab); + } else { + showToast('❌ ' + (res.message || '提交失败,请重试'), 'error'); + } + }) + .catch(() => { + showToast('❌ 网络异常,请重试', 'error'); + }) + .finally(() => { + if (submitBtn) { + submitBtn.disabled = false; + submitBtn.textContent = '✈️ 提交反馈'; + } + }); +} + +// ── 赞同/取消赞同 ── +function toggleVote(id) { + const card = $list?.querySelector(`[data-fb-id="${id}"]`); + const voteBtn = card?.querySelector('[data-fb-vote]'); + if (!voteBtn) return; + + // 乐观更新 + const wasVoted = voteBtn.classList.contains('voted'); + const countSpan = voteBtn.querySelector('[data-fb-vote-count]'); + let prevCount = parseInt(countSpan?.textContent || '0'); + + voteBtn.classList.toggle('voted'); + voteBtn.innerHTML = (wasVoted ? '👆' : '👍') + ' ' + (wasVoted ? prevCount - 1 : prevCount + 1) + ''; + + fetch(getVoteUrl(id), { + method: 'POST', + headers: { + 'X-CSRF-TOKEN': getCsrfToken(), + 'Accept': 'application/json', + }, + }) + .then(r => r.json()) + .then(res => { + if (res.status === 'success') { + voteBtn.innerHTML = (res.voted ? '👍' : '👆') + ' ' + res.votes_count + ''; + if (res.voted) { + voteBtn.classList.add('voted'); + } else { + voteBtn.classList.remove('voted'); + } + } else { + // 回滚 + voteBtn.classList.toggle('voted'); + voteBtn.innerHTML = (wasVoted ? '👍' : '👆') + ' ' + prevCount + ''; + showToast('❌ ' + (res.message || '操作失败'), 'error'); + } + }) + .catch(() => { + // 回滚 + voteBtn.classList.toggle('voted'); + voteBtn.innerHTML = (wasVoted ? '👍' : '👆') + ' ' + prevCount + ''; + }); +} + +// ── 提交评论 ── +function submitReply(id) { + const input = $list?.querySelector(`[data-fb-reply-input="${id}"]`); + const btn = $list?.querySelector(`[data-fb-reply-btn="${id}"]`); + if (!input || !btn) return; + + const content = input.value.trim(); + if (!content) return; + + btn.disabled = true; + btn.textContent = '发送中…'; + + fetch(getReplyUrl(id), { + method: 'POST', + headers: { + 'X-CSRF-TOKEN': getCsrfToken(), + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify({ content }), + }) + .then(r => r.json()) + .then(res => { + if (res.status === 'success' && res.reply) { + // 追加评论到列表 + const repliesContainer = $list?.querySelector(`[data-fb-replies="${id}"]`); + if (repliesContainer) { + repliesContainer.insertAdjacentHTML('beforeend', buildReplyHtml(res.reply)); + } + input.value = ''; + // 更新评论计数 + const card = $list?.querySelector(`[data-fb-id="${id}"]`); + if (card) { + const countEl = card.querySelector('.fb-reply-count'); + if (countEl) { + const current = parseInt(countEl.textContent?.replace(/[^0-9]/g, '') || '0'); + countEl.textContent = '💬 ' + (current + 1); + } + } + showToast('✅ 评论已提交', 'success'); + } else { + showToast('❌ ' + (res.message || '评论失败'), 'error'); + } + }) + .catch(() => { + showToast('❌ 网络异常', 'error'); + }) + .finally(() => { + btn.disabled = false; + btn.textContent = '发送'; + }); +} + +// ── 删除反馈(通过 AJAX) ── +function deleteFeedback(id) { + if (!confirm('确定要删除这条反馈吗?')) return; + + fetch(getDestroyUrl(id), { + method: 'DELETE', + headers: { + 'X-CSRF-TOKEN': getCsrfToken(), + 'Accept': 'application/json', + }, + }) + .then(r => { + if (r.ok) { + return r.json(); + } + throw new Error('删除失败'); + }) + .then(res => { + if (res.status === 'success') { + showToast('✅ ' + (res.message || '反馈已删除'), 'success'); + const card = $list?.querySelector(`[data-fb-id="${id}"]`); + if (card) card.remove(); + // 检查列表是否为空 + if ($list && $list.querySelectorAll('.fb-card').length === 0) { + $list.innerHTML = '
💬暂无反馈
'; + } + } else { + showToast('❌ ' + (res.message || '删除失败'), 'error'); + } + }) + .catch(() => { + showToast('❌ 删除失败', 'error'); + }); +} + +// ── 展开/收起详情 ── +function toggleDetail(card) { + if (!card) return; + const detail = card.querySelector('.fb-detail'); + if (!detail) return; + const isVisible = detail.style.display !== 'none'; + // 收起其他已展开的 + $list?.querySelectorAll('.fb-card .fb-detail').forEach(d => { + if (d !== detail) d.style.display = 'none'; + }); + detail.style.display = isVisible ? 'none' : 'block'; + if (!isVisible) { + // 自动调整 textarea 高度 + const ta = card.querySelector('.fb-reply-form textarea'); + if (ta) { + ta.style.height = 'auto'; + ta.style.height = ta.scrollHeight + 'px'; + } + } +} + +// ── 事件绑定 ── +export function bindFeedbackControls() { + if (feedbackBound) return; + if (!cacheDom()) { + setTimeout(() => { + if (cacheDom()) bindFeedbackControls(); + }, 500); + return; + } + + feedbackBound = true; + + // 关闭按钮 + $closeBtn?.addEventListener('click', closeFeedbackModal); + + // 遮罩层点击关闭 + $modal?.addEventListener('click', (e) => { + if (e.target === $modal) closeFeedbackModal(); + }); + + // Tab 切换 + $tabs.forEach(btn => { + btn.addEventListener('click', () => { + const tab = btn.getAttribute('data-feedback-tab') || 'all'; + closeWriteForm(); + loadFeedbackData(tab); + }); + }); + + // 提交反馈按钮(底部) + $writeBtn?.addEventListener('click', openWriteForm); + + // 写表单关闭/取消 + $writeClose?.addEventListener('click', closeWriteForm); + $formCancel?.addEventListener('click', closeWriteForm); + + // 点击写表单遮罩关闭 + $writeOverlay?.addEventListener('click', (e) => { + if (e.target === $writeOverlay) closeWriteForm(); + }); + + // 表单提交 + $form?.addEventListener('submit', submitFeedbackForm); + + // ── 事件委托:列表内部交互 ── + $list?.addEventListener('click', (e) => { + // 卡片点击展开/收起(排除按钮) + const card = e.target.closest('.fb-card'); + if (card && !e.target.closest('button') && !e.target.closest('textarea')) { + toggleDetail(card); + return; + } + + // 赞同按钮 + const voteBtn = e.target.closest('[data-fb-vote]'); + if (voteBtn) { + const id = voteBtn.getAttribute('data-fb-vote'); + if (!voteBtn.disabled) toggleVote(id); + return; + } + + // 删除按钮 + const deleteBtn = e.target.closest('[data-fb-delete]'); + if (deleteBtn) { + const id = deleteBtn.getAttribute('data-fb-delete'); + deleteFeedback(id); + return; + } + + // 评论发送按钮 + const replyBtn = e.target.closest('[data-fb-reply-btn]'); + if (replyBtn) { + const id = replyBtn.getAttribute('data-fb-reply-btn'); + if (!replyBtn.disabled) submitReply(id); + return; + } + }); + + // ── 事件委托:列表滚动加载更多 ── + $list?.addEventListener('scroll', () => { + if (!$list) return; + const { scrollTop, scrollHeight, clientHeight } = $list; + if (scrollTop + clientHeight >= scrollHeight - 60) { + loadMoreFeedback(); + } + }); + + // ── 事件委托:评论输入框自动调整高度 ── + $list?.addEventListener('input', (e) => { + const ta = e.target.closest('[data-fb-reply-input]'); + if (ta) { + ta.style.height = 'auto'; + ta.style.height = Math.min(ta.scrollHeight, 80) + 'px'; + } + }); + + // ── 事件委托:评论输入框 Enter 发送 ── + $list?.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + const ta = e.target.closest('[data-fb-reply-input]'); + if (ta) { + e.preventDefault(); + const id = ta.getAttribute('data-fb-reply-input'); + submitReply(id); + } + } + }); + + // ESC 关闭 + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + if ($writeOverlay?.classList.contains('active')) { + closeWriteForm(); + } else if ($modal?.style.display === 'flex') { + closeFeedbackModal(); + } + } + }); +} + +// ── 工具函数 ── +function escapeHtml(str) { + if (!str) return ''; + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; +} + +function nl2br(str) { + if (!str) return ''; + return str.replace(/\n/g, '
'); +} diff --git a/resources/js/chat-room/toolbar.js b/resources/js/chat-room/toolbar.js index 5e7d46e..6802d61 100644 --- a/resources/js/chat-room/toolbar.js +++ b/resources/js/chat-room/toolbar.js @@ -22,6 +22,7 @@ export function runToolbarAction(action) { avatar: () => window.openAvatarPicker?.(), settings: () => window.openSettingsModal?.(), guestbook: () => window.openGuestbookModal?.(), + feedback: () => window.openFeedbackModal?.(), }; actions[action]?.(); diff --git a/resources/views/chat/frame.blade.php b/resources/views/chat/frame.blade.php index 01f3bca..f9e28de 100644 --- a/resources/views/chat/frame.blade.php +++ b/resources/views/chat/frame.blade.php @@ -225,6 +225,8 @@ @include('chat.partials.daily-sign-in-modal') {{-- 留言板弹窗 --}} @include('chat.partials.guestbook-modal') + {{-- 反馈弹窗 --}} + @include('chat.partials.feedback-modal') {{-- ═══════════ 游戏面板(partials/games/ 子目录,各自独立,包含 CSS + HTML + JS) ═══════════ --}} {{-- deferChatGameBootstrap 已迁移到 resources/js/chat-room/game-bootstrap.js --}} diff --git a/resources/views/chat/partials/feedback-modal.blade.php b/resources/views/chat/partials/feedback-modal.blade.php new file mode 100644 index 0000000..5ee75a2 --- /dev/null +++ b/resources/views/chat/partials/feedback-modal.blade.php @@ -0,0 +1,577 @@ +{{-- + 文件功能:用户反馈模态弹窗(仿留言板弹窗样式) + 供聊天室工具栏"反馈"按钮使用,替代跳转反馈页面。 + + 依赖 CSS:与留言板弹窗一致的蓝白风格 + 依赖 JS:resources/js/chat-room/feedback.js +--}} + + + +
+
+ {{-- 标题栏 --}} +
+
💬 用户反馈
+ +
+ + {{-- Toast --}} +
+ + {{-- Tab 导航 --}} +
+ + + +
+ + {{-- 反馈列表 --}} +
+
加载中…
+
+ + {{-- 加载更多指示 --}} + + + {{-- 底部提交按钮 --}} +
+ +
+ + {{-- 提交反馈表单(内嵌遮罩弹窗) --}} +
+
+
+
📝 提交反馈
+ +
+
+
+ @csrf +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
diff --git a/resources/views/chat/partials/layout/toolbar.blade.php b/resources/views/chat/partials/layout/toolbar.blade.php index f2f2f48..7bba583 100644 --- a/resources/views/chat/partials/layout/toolbar.blade.php +++ b/resources/views/chat/partials/layout/toolbar.blade.php @@ -26,7 +26,7 @@
头像
设置
-
反馈
+
反馈
留言
规则
diff --git a/routes/web.php b/routes/web.php index b568d9f..2a8ea93 100644 --- a/routes/web.php +++ b/routes/web.php @@ -366,6 +366,8 @@ Route::middleware(['chat.auth'])->group(function () { // ---- 用户反馈(独立前台页面 /feedback)---- // 反馈列表页 Route::get('/feedback', [FeedbackController::class, 'index'])->name('feedback.index'); + // 第一页数据(供聊天室模态弹窗用) + Route::get('/feedback/data', [FeedbackController::class, 'data'])->name('feedback.data'); // 懒加载接口:scroll 到底追加更多反馈 Route::get('/feedback/more', [FeedbackController::class, 'loadMore'])->name('feedback.more'); // 提交新反馈