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

299 lines
18 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.
{{--
文件功能:后台职务管理页面
按部门分组展示所有职务,支持新增/编辑/删除职务
编辑时可通过多选框配置该职务可任命的目标职务列表(任命白名单)
@author ChatRoom Laravel
@version 1.0.0
--}}
@extends('admin.layouts.app')
@section('title', '职务管理')
@section('content')
<div x-data="{
showForm: false,
editing: null,
selectedIds: [],
form: {
department_id: '',
name: '',
icon: '🎖️',
rank: 50,
level: 60,
max_persons: 1,
max_reward: '',
sort_order: 0
},
openCreate() {
this.editing = null;
this.selectedIds = [];
this.form = { department_id: '', name: '', icon: '🎖️', rank: 50, level: 60, max_persons: 1, max_reward: '', sort_order: 0 };
this.showForm = true;
},
openEdit(pos, appointableIds) {
this.editing = pos;
this.selectedIds = appointableIds;
this.form = {
department_id: pos.department_id,
name: pos.name,
icon: pos.icon || '',
rank: pos.rank,
level: pos.level,
max_persons: pos.max_persons || '',
max_reward: pos.max_reward || '',
sort_order: pos.sort_order,
};
this.showForm = true;
},
toggleId(id) {
if (this.selectedIds.includes(id)) {
this.selectedIds = this.selectedIds.filter(i => i !== id);
} else {
this.selectedIds.push(id);
}
},
isSelected(id) {
return this.selectedIds.includes(id);
}
}">
{{-- 头部 --}}
<div class="flex justify-between items-center mb-6">
<div>
<h2 class="text-lg font-bold text-gray-800">职务管理</h2>
<p class="text-sm text-gray-500">管理各部门职务,配置等级、图标、人数上限和任命权限</p>
</div>
<div class="flex space-x-2">
<a href="{{ route('admin.departments.index') }}"
style="background-color:#e5e7eb;color:#374151;padding:0.5rem 1rem;border-radius:0.5rem;font-weight:700;font-size:0.875rem;display:inline-flex;align-items:center;text-decoration:none;"
onmouseover="this.style.backgroundColor='#d1d5db'" onmouseout="this.style.backgroundColor='#e5e7eb'">
部门管理
</a>
<a href="{{ route('admin.appointments.index') }}"
style="background-color:#f97316;color:#fff;padding:0.5rem 1rem;border-radius:0.5rem;font-weight:700;font-size:0.875rem;display:inline-flex;align-items:center;text-decoration:none;box-shadow:0 1px 2px rgba(0,0,0,.1);"
onmouseover="this.style.backgroundColor='#ea580c'" onmouseout="this.style.backgroundColor='#f97316'">
🎖️ 任命管理
</a>
@if (Auth::id() === 1)
<button @click="openCreate()"
style="background-color:#4f46e5;color:#fff;padding:0.5rem 1.25rem;border-radius:0.5rem;font-weight:700;border:none;cursor:pointer;box-shadow:0 1px 2px rgba(0,0,0,.1);"
onmouseover="this.style.backgroundColor='#4338ca'"
onmouseout="this.style.backgroundColor='#4f46e5'">
+ 新增职务
</button>
@endif
</div>
</div>
@if (session('success'))
<div class="mb-4 px-4 py-3 bg-green-50 border border-green-200 text-green-700 rounded-lg text-sm">
{{ session('success') }}</div>
@endif
@if (session('error'))
<div class="mb-4 px-4 py-3 bg-red-50 border border-red-200 text-red-700 rounded-lg text-sm">
{{ session('error') }}</div>
@endif
{{-- 按部门分组展示职务 --}}
@foreach ($departments as $dept)
<div class="mb-8">
<div class="flex items-center space-x-3 mb-3">
<div class="w-3 h-3 rounded-full" style="background-color: {{ $dept->color }}"></div>
<h3 class="font-bold text-base" style="color: {{ $dept->color }}">{{ $dept->name }}</h3>
<span class="text-xs text-gray-400">位阶 {{ $dept->rank }}</span>
</div>
<div class="bg-white rounded-xl shadow-sm border overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50 text-gray-600 text-xs">
<tr>
<th class="px-4 py-3 text-left">图标</th>
<th class="px-4 py-3 text-left">职务名</th>
<th class="px-4 py-3 text-center">位阶</th>
<th class="px-4 py-3 text-center">等级</th>
<th class="px-4 py-3 text-center">人数上限</th>
<th class="px-4 py-3 text-center">当前在职</th>
<th class="px-4 py-3 text-center">奖励上限</th>
<th class="px-4 py-3 text-center">任命权</th>
@php $superLvl = (int) \App\Models\Sysparam::getValue('superlevel', '100'); @endphp
@if (Auth::user()->user_level >= $superLvl)
<th class="px-4 py-3 text-right">操作</th>
@endif
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@forelse ($dept->positions as $pos)
@php $appointableIds = $pos->appointablePositions->pluck('id')->toArray(); @endphp
<tr class="hover:bg-gray-50 transition">
<td class="px-4 py-3 text-xl">{{ $pos->icon }}</td>
<td class="px-4 py-3 font-bold">{{ $pos->name }}</td>
<td class="px-4 py-3 text-center">
<span
class="text-xs bg-indigo-100 text-indigo-700 px-2 py-0.5 rounded font-mono">{{ $pos->rank }}</span>
</td>
<td class="px-4 py-3 text-center">
<span
class="text-xs bg-orange-100 text-orange-700 px-2 py-0.5 rounded font-mono">Lv.{{ $pos->level }}</span>
</td>
<td class="px-4 py-3 text-center text-gray-600">{{ $pos->max_persons ?? '不限' }}</td>
<td class="px-4 py-3 text-center">
<span
class="{{ $pos->active_user_positions_count >= ($pos->max_persons ?? 999) ? 'text-red-600 font-bold' : 'text-indigo-600' }}">
{{ $pos->active_user_positions_count }}&nbsp;
</span>
</td>
<td class="px-4 py-3 text-center text-gray-600">
{{ $pos->max_reward ? number_format($pos->max_reward) . '金币' : '不限' }}
</td>
<td class="px-4 py-3 text-center">
@if (count($appointableIds) > 0)
<span
class="text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded">{{ count($appointableIds) }}
个职务</span>
@else
<span class="text-xs text-gray-400"></span>
@endif
</td>
<td class="px-4 py-3 text-right space-x-1">
@php $superLvl = (int) \App\Models\Sysparam::getValue('superlevel', '100'); @endphp
@if (Auth::user()->user_level >= $superLvl)
<button
@click="openEdit({
id: {{ $pos->id }},
department_id: {{ $pos->department_id }},
name: '{{ addslashes($pos->name) }}',
icon: '{{ $pos->icon }}',
rank: {{ $pos->rank }},
level: {{ $pos->level }},
max_persons: {{ $pos->max_persons ?? 'null' }},
max_reward: {{ $pos->max_reward ?? 'null' }},
sort_order: {{ $pos->sort_order }},
requestUrl: '{{ route('admin.positions.update', $pos->id) }}'
}, {{ json_encode($appointableIds) }})"
class="text-xs bg-indigo-50 text-indigo-600 font-bold px-2 py-1 rounded hover:bg-indigo-600 hover:text-white transition">
编辑
</button>
@endif
@if (Auth::id() === 1)
<form action="{{ route('admin.positions.destroy', $pos->id) }}" method="POST"
class="inline" onsubmit="return confirm('确定删除职务【{{ $pos->name }}】?')">
@csrf @method('DELETE')
<button type="submit"
class="text-xs bg-red-50 text-red-600 font-bold px-2 py-1 rounded hover:bg-red-600 hover:text-white transition">
删除
</button>
</form>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="9" class="px-4 py-6 text-center text-gray-400">该部门暂无职务</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
@endforeach
{{-- 新增/编辑弹窗 --}}
<div x-show="showForm" style="display: none;"
class="fixed inset-0 z-50 bg-black/60 flex items-center justify-center p-4">
<div @click.away="showForm = false"
class="bg-white rounded-xl shadow-2xl w-full max-w-2xl max-h-[92vh] overflow-y-auto" x-transition>
<div class="bg-indigo-900 px-6 py-4 flex justify-between items-center rounded-t-xl text-white sticky top-0">
<h3 class="font-bold text-lg" x-text="editing ? '编辑职务:' + editing.name : '新增职务'"></h3>
<button @click="showForm = false" class="text-gray-400 hover:text-white text-xl">&times;</button>
</div>
<div class="p-6">
<form :action="editing ? editing.requestUrl : '{{ route('admin.positions.store') }}'" method="POST">
@csrf
<template x-if="editing"><input type="hidden" name="_method" value="PUT"></template>
<div class="grid grid-cols-2 gap-4 mb-4">
<div class="col-span-2">
<label class="block text-xs font-bold text-gray-600 mb-1">所属部门</label>
<select name="department_id" x-model="form.department_id" required
class="w-full border rounded-md p-2 text-sm">
<option value="">-- 请选择部门 --</option>
@foreach ($departments as $dept)
<option value="{{ $dept->id }}">{{ $dept->name }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">职务名称</label>
<input type="text" name="name" x-model="form.name" required maxlength="50"
class="w-full border rounded-md p-2 text-sm">
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">图标Emoji</label>
<input type="text" name="icon" x-model="form.icon" maxlength="10"
class="w-full border rounded-md p-2 text-sm">
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">位阶0~99,跨全局排序)</label>
<input type="number" name="rank" x-model="form.rank" required min="0"
max="99" class="w-full border rounded-md p-2 text-sm">
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">等级user_level1~100</label>
<input type="number" name="level" x-model="form.level" required min="1"
max="100" class="w-full border rounded-md p-2 text-sm">
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">人数上限(空=不限)</label>
<input type="number" name="max_persons" x-model="form.max_persons" min="1"
class="w-full border rounded-md p-2 text-sm" placeholder="留空不限">
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">单次奖励上限金币(空=不限)</label>
<input type="number" name="max_reward" x-model="form.max_reward" min="0"
class="w-full border rounded-md p-2 text-sm" placeholder="留空不限">
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">排序</label>
<input type="number" name="sort_order" x-model="form.sort_order" required
min="0" class="w-full border rounded-md p-2 text-sm">
</div>
</div>
{{-- 任命白名单多选 --}}
<div class="border rounded-lg p-4 bg-gray-50">
<h4 class="text-xs font-bold text-gray-700 mb-2">
任命权限白名单
<span class="font-normal text-gray-400 ml-1">(勾选后此职务持有者可将用户任命到以下职务;不勾选则该职务无任命权)</span>
</h4>
<div class="grid grid-cols-2 gap-1 max-h-52 overflow-y-auto">
@foreach ($allPositions as $ap)
<label
class="flex items-center space-x-2 cursor-pointer hover:bg-white rounded p-1.5 text-sm"
:class="isSelected({{ $ap->id }}) ? 'bg-indigo-50 text-indigo-700 font-bold' :
'text-gray-700'">
<input type="checkbox" name="appointable_ids[]" value="{{ $ap->id }}"
:checked="isSelected({{ $ap->id }})"
@change="toggleId({{ $ap->id }})" class="rounded text-indigo-600">
<span>{{ $ap->department->name }}·{{ $ap->name }}</span>
</label>
@endforeach
</div>
</div>
<div class="flex justify-end space-x-3 pt-4 mt-4 border-t">
<button type="button" @click="showForm = false"
class="px-4 py-2 border rounded font-medium text-gray-600 hover:bg-gray-50">取消</button>
<button type="submit"
class="px-4 py-2 bg-indigo-600 text-white rounded font-bold hover:bg-indigo-700 shadow-sm"
x-text="editing ? '保存修改' : '创建职务'"></button>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection