新增:聊天室反馈模态弹窗(仿留言弹窗样式)
点击工具栏「反馈」按钮弹出反馈弹窗,不再跳转新页面。 新建文件: - 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() 方法
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 = '<div class="fb-loading">加载中…</div>';
|
||||
$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 = '<div class="fb-empty"><span class="fb-empty-icon">💬</span>暂无反馈</div>';
|
||||
$loader?.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
renderFeedbackList(items);
|
||||
updateLoader();
|
||||
})
|
||||
.catch(() => {
|
||||
$list.innerHTML = '<div class="fb-empty"><span class="fb-empty-icon">😵</span>数据加载失败</div>';
|
||||
$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 += `<button class="${voteBtnClass}" data-fb-vote="${item.id}" ${voteDisabled} title="${item.is_owner ? '不能赞同自己的反馈' : '赞同'}">${voteIcon} <span data-fb-vote-count="${item.id}">${item.votes_count}</span></button>`;
|
||||
|
||||
// 删除按钮
|
||||
if (item.can_delete) {
|
||||
actionsHtml += `<button class="fb-delete-btn" data-fb-delete="${item.id}">删除</button>`;
|
||||
}
|
||||
|
||||
// 展开评论数
|
||||
const repliesLabel = item.replies_count > 0 ? `💬 ${item.replies_count}` : '💬 0';
|
||||
|
||||
return `<div class="fb-card" data-fb-id="${item.id}">
|
||||
<div class="fb-card-header">
|
||||
<span class="fb-type-badge ${typeClass}">${escapeHtml(item.type_label)}</span>
|
||||
<span class="${statusClass}" style="${statusStyle}">${escapeHtml(item.status_label)}</span>
|
||||
<span class="fb-reply-count">${repliesLabel}</span>
|
||||
</div>
|
||||
<div class="fb-card-title">${escapeHtml(item.title)}</div>
|
||||
<div class="fb-card-meta">${escapeHtml(item.username)} · ${escapeHtml(item.created_at)}</div>
|
||||
<div class="fb-card-footer-actions">${actionsHtml}</div>
|
||||
|
||||
<div class="fb-detail" style="display:none;">
|
||||
<div class="fb-detail-content">${nl2br(escapeHtml(item.content))}</div>
|
||||
${item.admin_remark ? `<div class="fb-admin-remark"><div class="fb-admin-remark-label">🛡️ 开发者官方回复</div>${nl2br(escapeHtml(item.admin_remark))}</div>` : ''}
|
||||
<div class="fb-replies" data-fb-replies="${item.id}">
|
||||
${(item.replies || []).map(r => buildReplyHtml(r)).join('')}
|
||||
</div>
|
||||
<div class="fb-reply-form">
|
||||
<textarea data-fb-reply-input="${item.id}" placeholder="补充说明…" maxlength="1000" rows="1"></textarea>
|
||||
<button data-fb-reply-btn="${item.id}">发送</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function buildReplyHtml(reply) {
|
||||
const adminClass = reply.is_admin ? 'admin' : '';
|
||||
const badgeHtml = reply.is_admin ? '<span class="fb-reply-admin-badge">开发者</span>' : '';
|
||||
return `<div class="fb-reply-item ${adminClass}">
|
||||
<div class="fb-reply-header">
|
||||
<span class="fb-reply-username">${escapeHtml(reply.username)}</span>
|
||||
${badgeHtml}
|
||||
<span class="fb-reply-time">${escapeHtml(reply.created_at)}</span>
|
||||
</div>
|
||||
<div class="fb-reply-body">${nl2br(escapeHtml(reply.content))}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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 ? '👆' : '👍') + ' <span data-fb-vote-count="' + id + '">' + (wasVoted ? prevCount - 1 : prevCount + 1) + '</span>';
|
||||
|
||||
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 ? '👍' : '👆') + ' <span data-fb-vote-count="' + id + '">' + res.votes_count + '</span>';
|
||||
if (res.voted) {
|
||||
voteBtn.classList.add('voted');
|
||||
} else {
|
||||
voteBtn.classList.remove('voted');
|
||||
}
|
||||
} else {
|
||||
// 回滚
|
||||
voteBtn.classList.toggle('voted');
|
||||
voteBtn.innerHTML = (wasVoted ? '👍' : '👆') + ' <span data-fb-vote-count="' + id + '">' + prevCount + '</span>';
|
||||
showToast('❌ ' + (res.message || '操作失败'), 'error');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 回滚
|
||||
voteBtn.classList.toggle('voted');
|
||||
voteBtn.innerHTML = (wasVoted ? '👍' : '👆') + ' <span data-fb-vote-count="' + id + '">' + prevCount + '</span>';
|
||||
});
|
||||
}
|
||||
|
||||
// ── 提交评论 ──
|
||||
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 = '<div class="fb-empty"><span class="fb-empty-icon">💬</span>暂无反馈</div>';
|
||||
}
|
||||
} 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, '<br>');
|
||||
}
|
||||
@@ -22,6 +22,7 @@ export function runToolbarAction(action) {
|
||||
avatar: () => window.openAvatarPicker?.(),
|
||||
settings: () => window.openSettingsModal?.(),
|
||||
guestbook: () => window.openGuestbookModal?.(),
|
||||
feedback: () => window.openFeedbackModal?.(),
|
||||
};
|
||||
|
||||
actions[action]?.();
|
||||
|
||||
@@ -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 --}}
|
||||
|
||||
@@ -0,0 +1,577 @@
|
||||
{{--
|
||||
文件功能:用户反馈模态弹窗(仿留言板弹窗样式)
|
||||
供聊天室工具栏"反馈"按钮使用,替代跳转反馈页面。
|
||||
|
||||
依赖 CSS:与留言板弹窗一致的蓝白风格
|
||||
依赖 JS:resources/js/chat-room/feedback.js
|
||||
--}}
|
||||
|
||||
<style>
|
||||
/* 反馈弹窗遮罩 */
|
||||
#feedback-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, .5);
|
||||
z-index: 9999;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 弹窗主体 */
|
||||
#feedback-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;
|
||||
}
|
||||
|
||||
/* 标题栏 */
|
||||
#feedback-modal-header {
|
||||
background: linear-gradient(135deg, #336699, #5a8fc0);
|
||||
color: #fff;
|
||||
padding: 10px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#feedback-modal-title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#feedback-modal-close {
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
opacity: .8;
|
||||
transition: opacity .15s;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
#feedback-modal-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Toast */
|
||||
#feedback-toast {
|
||||
display: none;
|
||||
margin: 6px 12px 0;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Tab 导航 */
|
||||
#feedback-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #cde;
|
||||
flex-shrink: 0;
|
||||
background: #eef4fb;
|
||||
}
|
||||
.feedback-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;
|
||||
}
|
||||
.feedback-tab.active { color: #336699 !important; border-bottom-color: #336699 !important; }
|
||||
.feedback-tab:hover { color: #5a8fc0; }
|
||||
|
||||
/* 反馈列表容器 */
|
||||
#feedback-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px 12px;
|
||||
background: #f6faff;
|
||||
}
|
||||
|
||||
/* 反馈卡片 */
|
||||
.fb-card {
|
||||
background: #fff;
|
||||
border: 1px solid #d0e4f5;
|
||||
border-radius: 6px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 8px;
|
||||
transition: border-color .2s, box-shadow .2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.fb-card:hover {
|
||||
border-color: #5a8fc0;
|
||||
box-shadow: 0 2px 8px rgba(51, 102, 153, .18);
|
||||
}
|
||||
|
||||
.fb-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.fb-type-badge {
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
padding: 1px 6px;
|
||||
border-radius: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.fb-type-badge.bug { background: #ffe4e6; color: #be123c; }
|
||||
.fb-type-badge.suggestion { background: #dbeafe; color: #1d4ed8; }
|
||||
|
||||
.fb-status-badge {
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
padding: 1px 6px;
|
||||
border-radius: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fb-vote-count {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fb-reply-count {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.fb-card-title {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: #225588;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.4;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.fb-card-meta {
|
||||
font-size: 11px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* 展开详情区 */
|
||||
.fb-detail {
|
||||
border-top: 1px solid #eef4fb;
|
||||
padding: 10px 0 0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.fb-detail-content {
|
||||
font-size: 12px;
|
||||
color: #374151;
|
||||
line-height: 1.6;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
padding: 6px 0 8px;
|
||||
}
|
||||
|
||||
.fb-admin-remark {
|
||||
background: #eef4fb;
|
||||
border-left: 3px solid #336699;
|
||||
padding: 8px 10px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
color: #225588;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.fb-admin-remark-label {
|
||||
font-weight: bold;
|
||||
font-size: 11px;
|
||||
color: #336699;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* 评论列表 */
|
||||
.fb-replies {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.fb-reply-item {
|
||||
background: #f6faff;
|
||||
border-radius: 4px;
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.fb-reply-item.admin {
|
||||
background: #eef4fb;
|
||||
border: 1px solid #cde;
|
||||
}
|
||||
.fb-reply-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 3px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.fb-reply-username {
|
||||
font-weight: bold;
|
||||
color: #336699;
|
||||
}
|
||||
.fb-reply-admin-badge {
|
||||
font-size: 10px;
|
||||
background: #336699;
|
||||
color: #fff;
|
||||
padding: 0 5px;
|
||||
border-radius: 6px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.fb-reply-time {
|
||||
color: #9ca3af;
|
||||
margin-left: auto;
|
||||
}
|
||||
.fb-reply-body {
|
||||
color: #374151;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 评论输入区 */
|
||||
.fb-reply-form {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.fb-reply-form textarea {
|
||||
flex: 1;
|
||||
padding: 5px 8px;
|
||||
border: 1px solid #cde;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
outline: none;
|
||||
resize: vertical;
|
||||
min-height: 32px;
|
||||
max-height: 80px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.fb-reply-form textarea:focus {
|
||||
border-color: #336699;
|
||||
}
|
||||
.fb-reply-form button {
|
||||
padding: 5px 10px;
|
||||
background: linear-gradient(135deg, #336699, #5a8fc0);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: opacity .15s;
|
||||
white-space: nowrap;
|
||||
align-self: flex-end;
|
||||
}
|
||||
.fb-reply-form button:hover { opacity: .85; }
|
||||
.fb-reply-form button:disabled { opacity: .4; cursor: default; }
|
||||
|
||||
/* 删除按钮 */
|
||||
.fb-delete-btn {
|
||||
font-size: 10px;
|
||||
color: #dc2626;
|
||||
background: #fee2e2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: opacity .15s;
|
||||
}
|
||||
.fb-delete-btn:hover { opacity: .75; }
|
||||
|
||||
/* 卡片底部操作区 */
|
||||
.fb-card-footer-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.fb-vote-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
padding: 2px 8px;
|
||||
border: 1px solid #cde;
|
||||
border-radius: 4px;
|
||||
background: #f6faff;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
transition: all .15s;
|
||||
}
|
||||
.fb-vote-btn:hover {
|
||||
border-color: #5a8fc0;
|
||||
color: #336699;
|
||||
background: #eef4fb;
|
||||
}
|
||||
.fb-vote-btn.voted {
|
||||
background: #336699;
|
||||
color: #fff;
|
||||
border-color: #336699;
|
||||
}
|
||||
.fb-vote-btn.voted:hover {
|
||||
opacity: .85;
|
||||
}
|
||||
.fb-vote-btn:disabled {
|
||||
opacity: .4;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* 底部操作区 */
|
||||
#feedback-bottom-bar {
|
||||
flex-shrink: 0;
|
||||
padding: 8px 12px;
|
||||
background: #eef4fb;
|
||||
border-top: 1px solid #cde;
|
||||
text-align: center;
|
||||
}
|
||||
#feedback-submit-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%;
|
||||
}
|
||||
#feedback-submit-btn:hover { opacity: .85; }
|
||||
|
||||
/* 空状态 */
|
||||
.fb-empty {
|
||||
text-align: center;
|
||||
color: #9ca3af;
|
||||
padding: 40px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
.fb-empty-icon {
|
||||
font-size: 36px;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* 加载中 */
|
||||
.fb-loading {
|
||||
text-align: center;
|
||||
color: #6366f1;
|
||||
padding: 30px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 加载更多指示 */
|
||||
#feedback-loader {
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
#feedback-loader.hidden { display: none; }
|
||||
|
||||
/* ── 提交反馈表单(内嵌弹窗) ── */
|
||||
#feedback-write-overlay {
|
||||
display: none;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,.35);
|
||||
z-index: 10;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
#feedback-write-overlay.active { display: flex; }
|
||||
|
||||
#feedback-write-panel {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
width: 500px;
|
||||
max-width: 90vw;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#feedback-write-header {
|
||||
background: linear-gradient(135deg, #336699, #5a8fc0);
|
||||
color: #fff;
|
||||
padding: 10px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
#feedback-write-title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
flex: 1;
|
||||
}
|
||||
#feedback-write-close {
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
opacity: .8;
|
||||
transition: opacity .15s;
|
||||
line-height: 1;
|
||||
}
|
||||
#feedback-write-close:hover { opacity: 1; }
|
||||
|
||||
#feedback-write-body {
|
||||
padding: 12px 16px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.fb-form-row {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.fb-form-row label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #336699;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.fb-form-row select,
|
||||
.fb-form-row input[type="text"],
|
||||
.fb-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;
|
||||
}
|
||||
.fb-form-row select:focus,
|
||||
.fb-form-row input[type="text"]:focus,
|
||||
.fb-form-row textarea:focus {
|
||||
border-color: #336699;
|
||||
}
|
||||
.fb-form-row textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.fb-form-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
padding-top: 4px;
|
||||
}
|
||||
.fb-form-actions button {
|
||||
padding: 5px 14px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: opacity .15s;
|
||||
}
|
||||
.fb-form-actions button:hover { opacity: .85; }
|
||||
.fb-form-actions button:disabled { opacity: .4; cursor: default; }
|
||||
.fb-form-submit {
|
||||
background: linear-gradient(135deg, #336699, #5a8fc0);
|
||||
color: #fff;
|
||||
}
|
||||
.fb-form-cancel {
|
||||
background: #e5e7eb;
|
||||
color: #555;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="feedback-modal"
|
||||
data-feedback-data-url="{{ route('feedback.data') }}"
|
||||
data-feedback-more-url="{{ route('feedback.more') }}"
|
||||
data-feedback-store-url="{{ route('feedback.store') }}"
|
||||
data-feedback-vote-url-template="{{ route('feedback.vote', '__ID__') }}"
|
||||
data-feedback-reply-url-template="{{ route('feedback.reply', '__ID__') }}"
|
||||
data-feedback-destroy-url-template="{{ route('feedback.destroy', '__ID__') }}">
|
||||
<div id="feedback-modal-inner">
|
||||
{{-- 标题栏 --}}
|
||||
<div id="feedback-modal-header">
|
||||
<div id="feedback-modal-title">💬 用户反馈</div>
|
||||
<span id="feedback-modal-close" data-feedback-modal-close>✕</span>
|
||||
</div>
|
||||
|
||||
{{-- Toast --}}
|
||||
<div id="feedback-toast"></div>
|
||||
|
||||
{{-- Tab 导航 --}}
|
||||
<div id="feedback-tabs">
|
||||
<button class="feedback-tab active" data-feedback-tab="all">📋 全部</button>
|
||||
<button class="feedback-tab" data-feedback-tab="bug">🐛 Bug 报告</button>
|
||||
<button class="feedback-tab" data-feedback-tab="suggestion">💡 功能建议</button>
|
||||
</div>
|
||||
|
||||
{{-- 反馈列表 --}}
|
||||
<div id="feedback-list">
|
||||
<div class="fb-loading">加载中…</div>
|
||||
</div>
|
||||
|
||||
{{-- 加载更多指示 --}}
|
||||
<div id="feedback-loader" class="hidden"></div>
|
||||
|
||||
{{-- 底部提交按钮 --}}
|
||||
<div id="feedback-bottom-bar">
|
||||
<button id="feedback-submit-btn" data-feedback-write-btn>📝 提交反馈</button>
|
||||
</div>
|
||||
|
||||
{{-- 提交反馈表单(内嵌遮罩弹窗) --}}
|
||||
<div id="feedback-write-overlay">
|
||||
<div id="feedback-write-panel">
|
||||
<div id="feedback-write-header">
|
||||
<div id="feedback-write-title">📝 提交反馈</div>
|
||||
<span id="feedback-write-close" data-feedback-write-close>✕</span>
|
||||
</div>
|
||||
<div id="feedback-write-body">
|
||||
<form id="feedback-form" method="POST">
|
||||
@csrf
|
||||
<div class="fb-form-row">
|
||||
<label>反馈类型 <span style="color:#dc2626;">*</span></label>
|
||||
<select name="type" id="fb-type">
|
||||
<option value="bug">🐛 Bug 报告</option>
|
||||
<option value="suggestion">💡 功能建议</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="fb-form-row">
|
||||
<label>标题 <span style="color:#dc2626;">*</span></label>
|
||||
<input type="text" name="title" id="fb-title" maxlength="200" placeholder="一句话描述…" required>
|
||||
</div>
|
||||
<div class="fb-form-row">
|
||||
<label>详细描述 <span style="color:#dc2626;">*</span></label>
|
||||
<textarea name="content" id="fb-content" rows="5" maxlength="2000" required placeholder="请详细描述您遇到的问题或建议…"></textarea>
|
||||
</div>
|
||||
<div class="fb-form-actions">
|
||||
<button type="button" class="fb-form-cancel" data-fb-form-cancel>取消</button>
|
||||
<button type="submit" class="fb-form-submit">✈️ 提交反馈</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -26,7 +26,7 @@
|
||||
<div class="tool-btn" data-toolbar-action="avatar" title="修改头像">头像</div>
|
||||
<div class="tool-btn" data-toolbar-action="settings" title="个人设置">设置
|
||||
</div>
|
||||
<div class="tool-btn" data-toolbar-url="{{ route('feedback.index') }}" title="反馈">反馈</div>
|
||||
<div class="tool-btn" data-toolbar-action="feedback" title="反馈">反馈</div>
|
||||
<div class="tool-btn" data-toolbar-action="guestbook" title="留言板/私信">留言</div>
|
||||
<div class="tool-btn" data-toolbar-url="{{ route('guide') }}" title="规则/帮助">规则</div>
|
||||
|
||||
|
||||
@@ -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');
|
||||
// 提交新反馈
|
||||
|
||||
Reference in New Issue
Block a user