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

188 lines
10 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.
{{--
文件功能:后台用户反馈管理页面(仅 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