feat: 任命/撤销通知系统 + 用户名片UI优化

- 任命/撤销事件增加 type 字段区分类型
- 任命:全屏礼花 + 紫色弹窗 + 紫色系统消息
- 撤销:灰色弹窗 + 灰色系统消息,无礼花
- 消息分发:操作者/被操作者显示在私聊面板,其他人显示在公屏
- 系统消息加随机鼓励语(各5条轮换)
- ChatStateService 修复 Redis key 前缀扫描问题(getAllActiveRoomIds)
- 用户名片折叠优化:管理员视野、职务履历均可折叠
- 管理操作 + 职务操作合并为「🔧 管理操作」折叠区
- 悄悄话改为「🎁 送礼物」按钮,礼物面板内联展开
This commit is contained in:
2026-02-28 23:44:38 +08:00
parent a599047cf0
commit 5f30220609
80 changed files with 8579 additions and 473 deletions
+239
View File
@@ -0,0 +1,239 @@
{{--
文件功能:勤务台页面(职务管理荣誉展示台)
左侧子菜单:任职列表、日榜、周榜、月榜、总榜
URL/duty-hall?tab=roster|day|week|month|all
@extends layouts.app
--}}
@extends('layouts.app')
@section('title', '勤务台 · ' . ($tabs[$tab]['label'] ?? '任职列表') . ' - 飘落流星')
@section('nav-icon', '🏛️')
@section('nav-title', '勤务台')
@section('content')
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 flex gap-6 items-start">
{{-- ═══ 左侧子菜单 ═══ --}}
<aside
class="w-52 shrink-0 bg-white border border-gray-100 rounded-2xl shadow-sm overflow-hidden sticky top-20 self-start hidden md:block">
<div class="px-4 pt-4 pb-2">
<p class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-3">勤务台</p>
<nav class="space-y-1">
@foreach ($tabs as $key => $meta)
<a href="{{ route('duty-hall.index', ['tab' => $key]) }}"
class="flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-sm font-medium transition-colors
{{ $tab === $key
? 'bg-purple-50 text-purple-700 font-bold'
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-800' }}">
<span class="text-base">{{ $meta['icon'] }}</span>
<span>{{ $meta['label'] }}</span>
@if ($tab === $key)
<span class="ml-auto w-1.5 h-1.5 rounded-full bg-purple-600"></span>
@endif
</a>
@endforeach
</nav>
</div>
<div class="px-4 py-3 mt-2 border-t border-gray-50 text-xs text-gray-400 text-center">
光荣服务,公示透明
</div>
</aside>
{{-- ═══ 主内容区 ═══ --}}
<main class="flex-1 min-w-0">
{{-- ─── Tab:任职列表 ─── --}}
@if ($tab === 'roster')
<div class="mb-4">
<h2 class="text-lg font-bold text-gray-800">🏛️ 任职列表</h2>
<p class="text-xs text-gray-400 mt-0.5">按部门 · 职务展示当前全部在职人员</p>
</div>
@if ($currentStaff->isEmpty())
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm py-20 text-center text-gray-400">
<div class="text-5xl mb-4">📭</div>
<p>暂未设置任何部门</p>
</div>
@else
<div class="space-y-5">
@foreach ($currentStaff as $dept)
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden">
{{-- 部门标题 --}}
<div
class="px-5 py-3 bg-gradient-to-r from-purple-50 to-indigo-50 border-b border-gray-100 flex items-center gap-2">
<span class="text-base">{{ $dept->icon ?? '🏢' }}</span>
<span class="font-bold text-purple-700 text-sm">{{ $dept->name }}</span>
<span class="ml-auto text-xs text-gray-400">{{ $dept->positions->count() }} 个职务</span>
</div>
@if ($dept->positions->isEmpty())
<div class="px-5 py-4 text-xs text-gray-400 italic">该部门暂无职务设置</div>
@else
<div class="divide-y divide-gray-50">
@foreach ($dept->positions as $position)
<div class="px-5 py-3">
{{-- 职务名 --}}
<div class="flex items-center gap-1.5 mb-2">
<span class="text-sm">{{ $position->icon }}</span>
<span
class="text-sm font-semibold text-gray-700">{{ $position->name }}</span>
@php
$current = $position->activeUserPositions->count();
$maxSlots = $position->max_persons;
$isFull = $maxSlots !== null && $current >= $maxSlots;
@endphp
{{-- 名额计数标签 --}}
<span
class="ml-1 inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded-md text-[10px] font-bold
{{ $current === 0
? 'bg-gray-100 text-gray-400'
: ($isFull
? 'bg-red-100 text-red-600'
: 'bg-purple-100 text-purple-600') }}">
{{ $current }}
@if ($maxSlots !== null)
<span class="opacity-60">/{{ $maxSlots }}</span>
@endif
</span>
@if ($isFull)
<span class="text-[10px] text-red-400">已满</span>
@elseif ($current === 0)
<span class="text-[10px] text-gray-300">(暂缺)</span>
@endif
</div>
{{-- 在职人员列表 --}}
@if ($position->activeUserPositions->isNotEmpty())
<div class="flex flex-wrap gap-2">
@foreach ($position->activeUserPositions as $up)
<div
class="flex items-center gap-2 px-3 py-1.5 bg-gray-50 border border-gray-100 rounded-xl">
<img src="/images/headface/{{ strtolower($up->user?->headface ?? '1.gif') }}"
class="w-7 h-7 rounded-full border border-purple-100 object-cover bg-white"
onerror="this.src='/images/headface/1.gif'">
<div>
<p class="text-xs font-bold text-gray-800">
{{ $up->user?->username ?? '未知' }}</p>
<p class="text-[10px] text-gray-400">
{{ $up->appointed_at?->format('Y-m-d') }} ·
{{ $up->duration_days }} </p>
</div>
</div>
@endforeach
</div>
@else
{{-- 空缺占位 --}}
<div
class="flex items-center gap-2 px-3 py-2 border border-dashed border-gray-200 rounded-xl text-xs text-gray-300 w-fit">
<span>👤</span>
<span>暂无任职人员</span>
</div>
@endif
</div>
@endforeach
</div>
@endif
</div>
@endforeach
</div>
@endif
{{-- ─── Tab:日///总榜 ─── --}}
@else
@php
$tabMeta = $tabs[$tab];
$periodLabel = match ($tab) {
'day' => today()->format('Y年m月d日'),
'week' => now()->startOfWeek()->format('m月d日') . ' ' . now()->endOfWeek()->format('m月d日'),
'month' => now()->format('Y年m月'),
'all' => '历史累计',
};
@endphp
<div class="mb-4">
<h2 class="text-lg font-bold text-gray-800">{{ $tabMeta['icon'] }} 勤务{{ $tabMeta['label'] }}</h2>
<p class="text-xs text-gray-400 mt-0.5">{{ $periodLabel }} · 按在职期间登录时长排名</p>
</div>
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden">
@if ($leaderboard->isEmpty())
<div class="py-20 text-center text-gray-400">
<div class="text-5xl mb-4">📊</div>
<p>该时段暂无勤务记录</p>
</div>
@else
{{-- 表头 --}}
<div
class="grid grid-cols-12 gap-4 px-5 py-2.5 bg-gray-50 border-b border-gray-100 text-xs font-bold text-gray-400 uppercase">
<div class="col-span-1 text-center">名次</div>
<div class="col-span-5">成员</div>
<div class="col-span-3 text-right">在线时长</div>
<div class="col-span-3 text-right">签到次数</div>
</div>
@foreach ($leaderboard as $i => $row)
@php
$h = intdiv($row->total_seconds, 3600);
$m = intdiv($row->total_seconds % 3600, 60);
$medal = match ($i) {
0 => '🥇',
1 => '🥈',
2 => '🥉',
default => null,
};
@endphp
<div
class="grid grid-cols-12 gap-4 items-center px-5 py-3 border-b border-gray-50 last:border-0
{{ $i === 0 ? 'bg-yellow-50/40' : ($i === 1 ? 'bg-gray-50/60' : ($i === 2 ? 'bg-amber-50/40' : '')) }}
hover:bg-purple-50/30 transition-colors">
{{-- 名次 --}}
<div class="col-span-1 text-center">
@if ($medal)
<span class="text-lg">{{ $medal }}</span>
@else
<span class="text-sm font-bold text-gray-300">{{ $i + 1 }}</span>
@endif
</div>
{{-- 成员 --}}
<div class="col-span-5 flex items-center gap-3">
<img src="/images/headface/{{ strtolower($row->user?->headface ?? '1.gif') }}"
class="w-9 h-9 rounded-full border-2 border-purple-100 object-cover bg-white"
onerror="this.src='/images/headface/1.gif'">
<div>
<p class="font-bold text-sm text-gray-800">{{ $row->user?->username ?? '未知' }}</p>
<p class="text-xs text-gray-400">LV.{{ $row->user?->user_level ?? '' }}</p>
</div>
</div>
{{-- 时长 --}}
<div class="col-span-3 text-right">
<span class="text-sm font-bold text-purple-600 tabular-nums">{{ $h }}h
{{ $m }}m</span>
</div>
{{-- 次数 --}}
<div class="col-span-3 text-right">
<span class="text-sm font-bold text-indigo-500 tabular-nums">{{ $row->checkin_count }}
</span>
</div>
</div>
@endforeach
@endif
</div>
@endif
</main>
</div>
{{-- 移动端底部 Tab 导航 --}}
<div class="md:hidden fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 flex z-30">
@foreach ($tabs as $key => $meta)
<a href="{{ route('duty-hall.index', ['tab' => $key]) }}"
class="flex-1 flex flex-col items-center py-2 text-xs
{{ $tab === $key ? 'text-purple-600 font-bold' : 'text-gray-500' }}">
<span class="text-lg">{{ $meta['icon'] }}</span>
<span class="mt-0.5">{{ $meta['label'] }}</span>
</a>
@endforeach
</div>
@endsection