- 任命/撤销事件增加 type 字段区分类型 - 任命:全屏礼花 + 紫色弹窗 + 紫色系统消息 - 撤销:灰色弹窗 + 灰色系统消息,无礼花 - 消息分发:操作者/被操作者显示在私聊面板,其他人显示在公屏 - 系统消息加随机鼓励语(各5条轮换) - ChatStateService 修复 Redis key 前缀扫描问题(getAllActiveRoomIds) - 用户名片折叠优化:管理员视野、职务履历均可折叠 - 管理操作 + 职务操作合并为「🔧 管理操作」折叠区 - 悄悄话改为「🎁 送礼物」按钮,礼物面板内联展开
299 lines
18 KiB
PHP
299 lines
18 KiB
PHP
{{--
|
||
文件功能:后台职务管理页面
|
||
按部门分组展示所有职务,支持新增/编辑/删除职务
|
||
编辑时可通过多选框配置该职务可任命的目标职务列表(任命白名单)
|
||
|
||
@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 }} 人
|
||
</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">×</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_level,1~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
|