Files
chatroom/app/Http/Controllers/FeedbackController.php
T
pllx 62371a7c64 新增:聊天室反馈模态弹窗(仿留言弹窗样式)
点击工具栏「反馈」按钮弹出反馈弹窗,不再跳转新页面。

新建文件:
- 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() 方法
2026-04-28 10:29:14 +08:00

337 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 文件功能:用户反馈前台控制器
* 对应独立页面 /feedback,处理用户提交 Bug报告/功能建议、
* 赞同(Toggle)、补充评论、删除等操作
* 所有写操作均需登录(chat.auth 中间件保护)
*
* @author ChatRoom Laravel
*
* @version 1.0.0
*/
namespace App\Http\Controllers;
use App\Models\FeedbackItem;
use App\Models\FeedbackReply;
use App\Models\FeedbackVote;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
class FeedbackController extends Controller
{
/** 每次懒加载的条数 */
private const PAGE_SIZE = 10;
/**
* 用户反馈列表页(SSR首屏)
* 预加载按赞同数倒序的 10 条反馈
*/
public function index(): View
{
$feedbacks = FeedbackItem::with(['replies'])
->orderByDesc('votes_count')
->orderByDesc('created_at')
->limit(self::PAGE_SIZE)
->get();
// 当前用户已赞同的反馈 ID 集合(前端切换按钮状态用)
$myVotedIds = FeedbackVote::where('user_id', Auth::id())
->whereIn('feedback_id', $feedbacks->pluck('id'))
->pluck('feedback_id')
->toArray();
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
*
* @param Request $request 含 after_id / type 筛选参数
*/
public function loadMore(Request $request): JsonResponse
{
$afterId = (int) $request->input('after_id', PHP_INT_MAX);
$type = $request->input('type'); // bug|suggestion|null(全部)
$query = FeedbackItem::with(['replies'])
->where('id', '<', $afterId)
->orderByDesc('votes_count')
->orderByDesc('created_at');
if ($type && in_array($type, ['bug', 'suggestion'])) {
$query->ofType($type);
}
$items = $query->limit(self::PAGE_SIZE)->get();
// 当前用户已赞同的 ID(用于切换按钮状态)
$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),
'has_more' => $items->count() === self::PAGE_SIZE,
]);
}
/**
* 提交新反馈(Bug报告或功能建议)
*
* @param Request $request 含 type/title/content 字段
*/
public function store(Request $request): JsonResponse
{
$data = $request->validate([
'type' => 'required|in:bug,suggestion',
'title' => 'required|string|max:200',
'content' => 'required|string|max:2000',
]);
/** @var \App\Models\User $user */
$user = Auth::user();
$item = FeedbackItem::create([
'user_id' => $user->id,
'username' => $user->username,
'type' => $data['type'],
'title' => $data['title'],
'content' => $data['content'],
'status' => 'pending',
]);
return response()->json([
'status' => 'success',
'message' => '反馈已提交,感谢您的贡献!',
'item' => $this->formatItem($item, false),
]);
}
/**
* 赞同/取消赞同反馈(Toggle 操作)
* 每人每条只能赞同一次,再次点击则取消
* 使用数据库事务保证 votes_count 冗余字段与记录一致
*
* @param int $id 反馈 ID
*/
public function vote(int $id): JsonResponse
{
$feedback = FeedbackItem::findOrFail($id);
$userId = Auth::id();
// 不能赞同自己提交的反馈
if ($feedback->user_id === $userId) {
return response()->json([
'status' => 'error',
'message' => '不能赞同自己的反馈',
], 422);
}
$voted = false;
DB::transaction(function () use ($feedback, $userId, &$voted): void {
$existing = FeedbackVote::where('feedback_id', $feedback->id)
->where('user_id', $userId)
->first();
if ($existing) {
// 已赞同 → 取消赞同
$existing->delete();
$feedback->decrement('votes_count');
$voted = false;
} else {
// 未赞同 → 新增赞同
FeedbackVote::create([
'feedback_id' => $feedback->id,
'user_id' => $userId,
]);
$feedback->increment('votes_count');
$voted = true;
}
});
return response()->json([
'status' => 'success',
'voted' => $voted,
'votes_count' => $feedback->fresh()->votes_count,
]);
}
/**
* 提交补充评论
* id=1 管理员的回复自动标记 is_admin=true(前台特殊展示)
*
* @param Request $request 含 content 字段
* @param int $id 反馈 ID
*/
public function reply(Request $request, int $id): JsonResponse
{
$feedback = FeedbackItem::findOrFail($id);
$data = $request->validate([
'content' => 'required|string|max:1000',
]);
/** @var \App\Models\User $user */
$user = Auth::user();
/** @var FeedbackReply $reply */
$reply = null;
DB::transaction(function () use ($feedback, $data, $user, &$reply): void {
$reply = FeedbackReply::create([
'feedback_id' => $feedback->id,
'user_id' => $user->id,
'username' => $user->username,
'content' => $data['content'],
'is_admin' => $user->id === 1,
]);
$feedback->increment('replies_count');
});
return response()->json([
'status' => 'success',
'message' => '评论已提交',
'reply' => [
'id' => $reply->id,
'username' => $reply->username,
'content' => $reply->content,
'is_admin' => $reply->is_admin,
'created_at' => $reply->created_at->diffForHumans(),
],
]);
}
/**
* 删除反馈
* 普通用户:仅24小时内可删除自己的反馈
* 管理员(id=1):任意时间可删除任意反馈
*
* @param int $id 反馈 ID
*/
public function destroy(int $id): JsonResponse
{
$feedback = FeedbackItem::findOrFail($id);
/** @var \App\Models\User $user */
$user = Auth::user();
$isOwner = $feedback->user_id === $user->id;
$isAdmin = $user->id === 1;
if (! $isOwner && ! $isAdmin) {
return response()->json(['status' => 'error', 'message' => '无权删除'], 403);
}
if ($isOwner && ! $isAdmin && ! $feedback->is_within_24_hours) {
return response()->json([
'status' => 'error',
'message' => '超过 24 小时的反馈无法删除',
], 422);
}
// 级联删除关联的赞同记录和评论记录
DB::transaction(function () use ($feedback): void {
FeedbackVote::where('feedback_id', $feedback->id)->delete();
FeedbackReply::where('feedback_id', $feedback->id)->delete();
$feedback->delete();
});
return response()->json(['status' => 'success', 'message' => '已删除']);
}
// ═══════════════ 私有辅助方法 ═══════════════
/**
* 格式化单条反馈数据(供 JSON 返回给前端)
*
* @param FeedbackItem $item 反馈实例
* @param bool $voted 当前用户是否已赞同
*/
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,
'type_label' => $item->type_label,
'title' => $item->title,
'content' => $item->content,
'status' => $item->status,
'status_label' => $item->status_label,
'status_color' => $item->status_config['color'],
'admin_remark' => $item->admin_remark,
'votes_count' => $item->votes_count,
'replies_count' => $item->replies_count,
'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,
'content' => $r->content,
'is_admin' => $r->is_admin,
'created_at' => $r->created_at->diffForHumans(),
])->values()->toArray(),
];
}
/**
* 批量格式化反馈数据集合
*
* @param \Illuminate\Support\Collection<int, FeedbackItem> $items
* @param array<int> $myVotedIds 当前用户已赞同的 ID 列表
*/
private function formatItems(\Illuminate\Support\Collection $items, array $myVotedIds): array
{
return $items->map(fn (FeedbackItem $item) => $this->formatItem(
$item,
in_array($item->id, $myVotedIds)
))->values()->toArray();
}
}