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.title)}
+
${escapeHtml(item.username)} · ${escapeHtml(item.created_at)}
+
+
+
+
${nl2br(escapeHtml(item.content))}
+ ${item.admin_remark ? `` : ''}
+
+ ${(item.replies || []).map(r => buildReplyHtml(r)).join('')}
+
+
+
+
+
+
+
`;
+}
+
+function buildReplyHtml(reply) {
+ const adminClass = reply.is_admin ? 'admin' : '';
+ const badgeHtml = reply.is_admin ? '开发者' : '';
+ return `
+
+
${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 导航 --}}
+
+
+
+
+
+
+ {{-- 反馈列表 --}}
+
+
+ {{-- 加载更多指示 --}}
+
+
+ {{-- 底部提交按钮 --}}
+
+
+
+
+ {{-- 提交反馈表单(内嵌遮罩弹窗) --}}
+
+
+
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');
// 提交新反馈