Files
chatroom/resources/views/admin/positions/index.blade.php
lkddi 41d4acdd72 修复:职务编辑表单中 0 值被 || '' 误转为空字符串
openEdit() 中三个奖励字段从 OR 运算符改为显式 null 检查:
- max_reward: pos.max_reward !== null ...
- daily_reward_limit 同上
- recipient_daily_limit 同上
- max_persons 改用 JS ?? 空值合并运算符

根本原因:0 在 JS 中是 falsy,pos.max_reward || '' 会将 0 变成 '',
提交到后端后 nullable 规则将空字符串解析为 null 覆盖掉用户的 0 设置。
2026-03-01 11:11:57 +08:00

317 lines
20 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: '',
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
{{-- 按部门分组展示职务 --}}
@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' }},
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">&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="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>
</div>
@endsection