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() 方法
337 lines
11 KiB
PHP
337 lines
11 KiB
PHP
<?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();
|
||
}
|
||
}
|