Files
chatroom/resources/views/admin/feedback/index.blade.php
T
lkddi 5f30220609 feat: 任命/撤销通知系统 + 用户名片UI优化
- 任命/撤销事件增加 type 字段区分类型
- 任命:全屏礼花 + 紫色弹窗 + 紫色系统消息
- 撤销:灰色弹窗 + 灰色系统消息,无礼花
- 消息分发:操作者/被操作者显示在私聊面板,其他人显示在公屏
- 系统消息加随机鼓励语(各5条轮换)
- ChatStateService 修复 Redis key 前缀扫描问题(getAllActiveRoomIds)
- 用户名片折叠优化:管理员视野、职务履历均可折叠
- 管理操作 + 职务操作合并为「🔧 管理操作」折叠区
- 悄悄话改为「🎁 送礼物」按钮,礼物面板内联展开
2026-02-28 23:44:38 +08:00

188 lines
10 KiB
PHP

{{--
文件功能:后台用户反馈管理页面(仅 id=1 超级管理员可访问)
列表展示所有用户提交的 Bug 报告和功能建议
支持按类型+状态筛选,可直接修改状态(Ajax)和填写官方回复
@extends admin.layouts.app
--}}
@extends('admin.layouts.app')
@section('title', '用户反馈管理')
@section('content')
<div class="flex justify-between items-center mb-6">
<div>
<h2 class="text-xl font-bold text-gray-800">
💬 用户反馈管理
@if ($pendingCount > 0)
<span class="ml-2 text-sm bg-orange-100 text-orange-700 font-bold px-2 py-0.5 rounded-full">
{{ $pendingCount }} 条待处理
</span>
@endif
</h2>
<p class="text-sm text-gray-500 mt-1">管理用户提交的 Bug 报告和功能建议,修改状态后前台实时更新</p>
</div>
</div>
{{-- 筛选栏 --}}
<div class="flex flex-wrap gap-3 mb-5" x-data="{
type: '{{ $currentType ?? '' }}',
status: '{{ $currentStatus ?? '' }}',
go() {
const params = new URLSearchParams();
if (this.type) params.set('type', this.type);
if (this.status) params.set('status', this.status);
window.location.href = '/admin/feedback?' + params.toString();
}
}">
<select x-model="type" @change="go()"
class="border border-gray-200 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-400 outline-none bg-white">
<option value="">所有类型</option>
<option value="bug">🐛 Bug报告</option>
<option value="suggestion">💡 功能建议</option>
</select>
<select x-model="status" @change="go()"
class="border border-gray-200 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-400 outline-none bg-white">
<option value="">所有状态</option>
@foreach ($statusConfig as $key => $config)
<option value="{{ $key }}">{{ $config['icon'] }} {{ $config['label'] }}</option>
@endforeach
</select>
@if ($currentType || $currentStatus)
<a href="{{ route('admin.feedback.index') }}"
class="px-3 py-2 text-sm text-gray-500 hover:text-gray-700 border border-gray-200 rounded-lg bg-white hover:bg-gray-50 transition">
清除筛选
</a>
@endif
</div>
{{-- 反馈列表 --}}
<div class="space-y-3">
@forelse($feedbacks as $feedback)
<div class="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden" x-data="{
expanded: false,
status: '{{ $feedback->status }}',
remark: @js($feedback->admin_remark ?? ''),
saving: false,
async updateStatus() {
this.saving = true;
const res = await fetch('/admin/feedback/{{ $feedback->id }}', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content,
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-HTTP-Method-Override': 'PUT',
},
body: JSON.stringify({ status: this.status, admin_remark: this.remark, _method: 'PUT' }),
});
const data = await res.json();
this.saving = false;
if (data.status !== 'success') alert('保存失败');
}
}">
{{-- 卡片头部 --}}
<div class="px-5 py-4 flex items-start gap-4">
{{-- 赞同数 --}}
<div class="shrink-0 text-center">
<div class="text-2xl font-black text-indigo-600">{{ $feedback->votes_count }}</div>
<div class="text-xs text-gray-400">赞同</div>
</div>
{{-- 类型+状态+标题 --}}
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 flex-wrap mb-1">
@php
$typeConfig = \App\Models\FeedbackItem::TYPE_CONFIG[$feedback->type] ?? null;
@endphp
<span
class="px-2 py-0.5 rounded text-xs font-bold {{ $feedback->type === 'bug' ? 'bg-rose-100 text-rose-700' : 'bg-blue-100 text-blue-700' }}">
{{ $typeConfig['label'] ?? '' }}
</span>
{{-- 状态下拉(Ajax 即时保存) --}}
<select x-model="status" @change="updateStatus()"
class="border border-gray-200 rounded px-2 py-0.5 text-xs font-bold focus:ring-1 focus:ring-indigo-400 outline-none cursor-pointer"
:disabled="saving">
@foreach ($statusConfig as $key => $config)
<option value="{{ $key }}"
{{ $feedback->status === $key ? 'selected' : '' }}>
{{ $config['icon'] }} {{ $config['label'] }}
</option>
@endforeach
</select>
<span class="text-gray-400 text-xs">💬 {{ $feedback->replies_count }}</span>
<span x-show="saving" class="text-xs text-indigo-500 animate-pulse">保存中...</span>
</div>
<h4 class="font-bold text-gray-800 text-sm">{{ $feedback->title }}</h4>
<p class="text-gray-400 text-xs mt-1">
by {{ $feedback->username }} · {{ $feedback->created_at->diffForHumans() }}
</p>
</div>
{{-- 展开按钮 --}}
<button @click="expanded = !expanded"
class="shrink-0 text-indigo-600 hover:text-indigo-800 text-sm font-bold border border-indigo-200 hover:border-indigo-400 px-3 py-1.5 rounded-lg transition">
<span x-text="expanded ? '收起 ▲' : '展开 ▽'"></span>
</button>
</div>
{{-- 展开详情 --}}
<div x-show="expanded" x-transition.opacity class="border-t border-gray-100">
{{-- 原始描述 --}}
<div class="px-5 py-4 bg-gray-50 text-sm text-gray-700 leading-relaxed">
<p class="font-bold text-gray-500 text-xs mb-2">用户描述</p>
<p class="whitespace-pre-wrap">{{ $feedback->content }}</p>
</div>
{{-- 所有补充评论 --}}
@if ($feedback->replies->count() > 0)
<div class="px-5 py-3 border-t border-gray-100 space-y-2">
<p class="text-xs font-bold text-gray-500 mb-2">用户补充 ({{ $feedback->replies->count() }} )</p>
@foreach ($feedback->replies as $reply)
<div
class="rounded-lg px-3 py-2 text-sm {{ $reply->is_admin ? 'bg-indigo-50 border border-indigo-200' : 'bg-gray-50' }}">
<div class="flex items-center gap-2 mb-1">
<span
class="font-bold {{ $reply->is_admin ? 'text-indigo-700' : 'text-gray-700' }}">{{ $reply->username }}</span>
@if ($reply->is_admin)
<span
class="text-xs bg-indigo-200 text-indigo-800 px-1.5 rounded font-bold">开发者</span>
@endif
<span
class="text-gray-400 text-xs">{{ $reply->created_at->diffForHumans() }}</span>
</div>
<p class="text-gray-700 whitespace-pre-wrap">{{ $reply->content }}</p>
</div>
@endforeach
</div>
@endif
{{-- 官方回复+保存区 --}}
<div class="px-5 py-4 border-t border-gray-100 bg-indigo-50/40">
<p class="text-xs font-bold text-indigo-800 mb-2">🛡️ 官方回复(公开显示给所有用户)</p>
<textarea x-model="remark" rows="3" placeholder="填写官方处理说明、修复进度或拒绝原因..."
class="w-full border border-indigo-200 rounded-lg p-2.5 text-sm resize-none focus:ring-2 focus:ring-indigo-400 outline-none bg-white"></textarea>
<div class="flex justify-end mt-2">
<button @click="updateStatus()" :disabled="saving"
class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg text-xs font-bold disabled:opacity-50 transition">
<span x-text="saving ? '保存中...' : '保存状态+回复'"></span>
</button>
</div>
</div>
</div>
</div>
@empty
<div class="bg-white rounded-xl border border-gray-100 py-16 text-center text-gray-400">
<p class="text-4xl mb-3">💬</p>
<p class="font-bold text-lg">暂无用户反馈</p>
<p class="text-sm mt-1">等待用户从前台提交问题和建议</p>
</div>
@endforelse
</div>
{{-- 分页 --}}
@if ($feedbacks->hasPages())
<div class="mt-6">
{{ $feedbacks->links() }}
</div>
@endif
@endsection