🪙 在多数平台/字体上渲染为银灰色,与「金币」语义不符; 💰 各平台均渲染为金黄色,更直观传达金币概念。 涉及文件(43处): - app/Jobs:百家乐、赛马结算广播 - app/Http/Controllers:管理员命令、红包、老虎机、神秘箱子 - app/Listeners - resources/views:聊天室各游戏面板、商店、toolbar、后台页面等
453 lines
28 KiB
PHP
453 lines
28 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: '',
|
||
daily_reward_limit: '',
|
||
recipient_daily_limit: '',
|
||
sort_order: 0
|
||
},
|
||
|
||
openCreate() {
|
||
this.editing = null;
|
||
this.selectedIds = [];
|
||
this.form = { department_id: '', name: '', icon: '🎖️', rank: 50, level: 60, max_persons: 1, max_reward: '', daily_reward_limit: '', recipient_daily_limit: '', 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 !== null && pos.max_reward !== undefined ? pos.max_reward : '',
|
||
daily_reward_limit: pos.daily_reward_limit !== null && pos.daily_reward_limit !== undefined ? pos.daily_reward_limit : '',
|
||
recipient_daily_limit: pos.recipient_daily_limit !== null && pos.recipient_daily_limit !== undefined ? pos.recipient_daily_limit : '',
|
||
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
|
||
|
||
{{-- 全局奖励接收上限配置卡片(失焦/回车自动保存) --}}
|
||
<div class="mb-6 bg-amber-50 border border-amber-200 rounded-xl p-5" x-data="{
|
||
val: {{ $globalRecipientDailyMax }},
|
||
saving: false,
|
||
saved: false,
|
||
error: '',
|
||
async save() {
|
||
if (this.saving) return;
|
||
this.saving = true;
|
||
this.saved = false;
|
||
this.error = '';
|
||
try {
|
||
const res = await fetch('{{ route('admin.positions.reward_config') }}', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]')?.content ||
|
||
'{{ csrf_token() }}',
|
||
'Accept': 'application/json',
|
||
},
|
||
body: JSON.stringify({ reward_recipient_daily_max: this.val }),
|
||
});
|
||
if (res.ok) {
|
||
this.saved = true;
|
||
setTimeout(() => this.saved = false, 3000);
|
||
} else {
|
||
const d = await res.json().catch(() => ({}));
|
||
this.error = d.message || '保存失败';
|
||
}
|
||
} catch {
|
||
this.error = '网络异常';
|
||
}
|
||
this.saving = false;
|
||
}
|
||
}">
|
||
<div class="flex items-center gap-6 flex-wrap">
|
||
<div class="flex-1 min-w-0">
|
||
<h3 class="text-sm font-bold text-amber-800 mb-1">💰 全局奖励接收上限</h3>
|
||
<p class="text-xs text-amber-700 leading-relaxed">
|
||
每位用户单日内可从<b>所有职务持有者</b>处累计接收奖励金币的最高次数。
|
||
设为 <code class="bg-amber-100 px-1 rounded">0</code> 表示不限制。
|
||
</p>
|
||
</div>
|
||
<div class="flex items-center gap-2 shrink-0">
|
||
<label class="text-xs text-amber-700 font-bold whitespace-nowrap">每日上限:</label>
|
||
<input type="number" x-model.number="val" min="0" max="9999" @blur="save()"
|
||
@keydown.enter.prevent="save()"
|
||
class="w-20 h-8 px-2 text-sm border border-amber-300 rounded-md bg-white text-amber-900
|
||
focus:outline-none focus:ring-2 focus:ring-amber-400 text-center">
|
||
<span class="text-xs text-amber-600 whitespace-nowrap">次(0=不限)</span>
|
||
{{-- 状态反馈 --}}
|
||
<span x-show="saving" class="text-xs text-amber-500 whitespace-nowrap">保存中…</span>
|
||
<span x-show="saved" class="text-xs text-green-600 font-bold whitespace-nowrap">✓ 已保存</span>
|
||
<span x-show="error" x-text="error" class="text-xs text-red-500 whitespace-nowrap"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- 按部门分组展示职务 --}}
|
||
@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>
|
||
<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-2 py-3 text-center" x-data="inlinePatch({{ $pos->id }}, 'max_persons', {{ $pos->max_persons ?? 'null' }})">
|
||
<input type="number" x-model.number="val" min="1" max="9999"
|
||
@blur="save()" @keydown.enter.prevent="$el.blur()" placeholder="不限"
|
||
class="w-16 text-center text-xs border-0 border-b border-dashed border-gray-300
|
||
bg-transparent focus:outline-none focus:border-indigo-400
|
||
text-gray-600 py-0.5">
|
||
<span x-show="saved" class="block text-green-500 text-xs leading-none">✓</span>
|
||
<span x-show="error" x-text="error"
|
||
class="block text-red-400 text-xs leading-none"></span>
|
||
</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-2 py-3 text-center" x-data="inlinePatch({{ $pos->id }}, 'max_reward', {{ $pos->max_reward ?? 'null' }})">
|
||
<input type="number" x-model.number="val" min="0" max="999999"
|
||
@blur="save()" @keydown.enter.prevent="$el.blur()" placeholder="不限"
|
||
class="w-16 text-center text-xs border-0 border-b border-dashed border-gray-300
|
||
bg-transparent focus:outline-none focus:border-amber-400
|
||
text-amber-700 py-0.5">
|
||
<span x-show="saved" class="block text-green-500 text-xs leading-none">✓</span>
|
||
<span x-show="error" x-text="error"
|
||
class="block text-red-400 text-xs leading-none"></span>
|
||
</td>
|
||
{{-- 单日发放总上限:内联编辑 --}}
|
||
<td class="px-2 py-3 text-center" x-data="inlinePatch({{ $pos->id }}, 'daily_reward_limit', {{ $pos->daily_reward_limit ?? 'null' }})">
|
||
<input type="number" x-model.number="val" min="0" max="999999"
|
||
@blur="save()" @keydown.enter.prevent="$el.blur()" placeholder="不限"
|
||
class="w-16 text-center text-xs border-0 border-b border-dashed border-gray-300
|
||
bg-transparent focus:outline-none focus:border-indigo-400
|
||
text-indigo-700 py-0.5">
|
||
<span x-show="saved" class="block text-green-500 text-xs leading-none">✓</span>
|
||
<span x-show="error" x-text="error"
|
||
class="block text-red-400 text-xs leading-none"></span>
|
||
</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' }},
|
||
daily_reward_limit: {{ $pos->daily_reward_limit ?? 'null' }},
|
||
recipient_daily_limit: {{ $pos->recipient_daily_limit ?? '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="daily_reward_limit" x-model="form.daily_reward_limit"
|
||
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="recipient_daily_limit" x-model="form.recipient_daily_limit"
|
||
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>
|
||
|
||
{{-- inlinePatch:职务列表内联编辑 Alpine 工厂函数(失焦/回车自动 PATCH 保存) --}}
|
||
<script>
|
||
function inlinePatch(positionId, field, initial) {
|
||
return {
|
||
val: initial, // null = 显示为空(placeholder "不限")
|
||
saving: false,
|
||
saved: false,
|
||
error: '',
|
||
|
||
async save() {
|
||
if (this.saving) return;
|
||
this.saving = true;
|
||
this.saved = false;
|
||
this.error = '';
|
||
try {
|
||
const body = {};
|
||
// 空字符串/null → 发 null(=不限)
|
||
body[field] = (this.val === '' || this.val === null) ? null : Number(this.val);
|
||
|
||
const res = await fetch(`/admin/positions/${positionId}/patch`, {
|
||
method: 'PATCH',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]')?.content || '',
|
||
'Accept': 'application/json',
|
||
},
|
||
body: JSON.stringify(body),
|
||
});
|
||
if (res.ok) {
|
||
this.saved = true;
|
||
setTimeout(() => this.saved = false, 2000);
|
||
} else {
|
||
const d = await res.json().catch(() => ({}));
|
||
this.error = d.message || '保存失败';
|
||
setTimeout(() => this.error = '', 3000);
|
||
}
|
||
} catch {
|
||
this.error = '网络异常';
|
||
setTimeout(() => this.error = '', 3000);
|
||
}
|
||
this.saving = false;
|
||
}
|
||
};
|
||
}
|
||
</script>
|
||
</div>
|
||
@endsection
|