- 任命/撤销事件增加 type 字段区分类型 - 任命:全屏礼花 + 紫色弹窗 + 紫色系统消息 - 撤销:灰色弹窗 + 灰色系统消息,无礼花 - 消息分发:操作者/被操作者显示在私聊面板,其他人显示在公屏 - 系统消息加随机鼓励语(各5条轮换) - ChatStateService 修复 Redis key 前缀扫描问题(getAllActiveRoomIds) - 用户名片折叠优化:管理员视野、职务履历均可折叠 - 管理操作 + 职务操作合并为「🔧 管理操作」折叠区 - 悄悄话改为「🎁 送礼物」按钮,礼物面板内联展开
188 lines
10 KiB
PHP
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
|