功能:奖励金币改为独立弹窗(展示额度信息)
- 点击「送金币」按钮打开独立弹窗,不再内联在用户名片中 - 弹窗展示 4 格额度信息:单次上限、单日上限、今日已发、剩余额度 - 新增 GET /command/reward-quota 接口(rewardQuota 方法) 返回当前操作人实时额度,超管返回全部不限 - 发放成功后页面内实时更新今日已发/剩余额度,无需刷新 - 移除原内联奖励面板,action 改为调用全局 openRewardModal()
This commit is contained in:
@@ -567,6 +567,56 @@ class AdminCommandController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前操作人的奖励额度信息(供发放弹窗展示)
|
||||
*
|
||||
* 返回字段:
|
||||
* - max_once: 单次上限(null = 不限)
|
||||
* - daily_limit: 单日发放总额上限(null = 不限)
|
||||
* - today_sent: 今日已发放总额
|
||||
* - daily_remaining: 今日剩余可发放额度(null = 不限)
|
||||
*/
|
||||
public function rewardQuota(): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$admin = Auth::user();
|
||||
$isSuperAdmin = $admin->id === 1;
|
||||
|
||||
if ($isSuperAdmin) {
|
||||
return response()->json([
|
||||
'max_once' => null,
|
||||
'daily_limit' => null,
|
||||
'today_sent' => 0,
|
||||
'daily_remaining' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
$position = $admin->activePosition?->position;
|
||||
if (! $position) {
|
||||
return response()->json([
|
||||
'max_once' => 0,
|
||||
'daily_limit' => null,
|
||||
'today_sent' => 0,
|
||||
'daily_remaining' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
// 今日已发放总额(以 user_id 统计操作人自己的发放)
|
||||
$todaySent = PositionAuthorityLog::where('user_id', $admin->id)
|
||||
->where('action_type', 'reward')
|
||||
->whereDate('created_at', today())
|
||||
->sum('amount');
|
||||
|
||||
$dailyLimit = $position->daily_reward_limit;
|
||||
$remaining = $dailyLimit !== null ? max(0, $dailyLimit - $todaySent) : null;
|
||||
|
||||
return response()->json([
|
||||
'max_once' => $position->max_reward, // null = 不限, 0 = 禁止, N = 有上限
|
||||
'daily_limit' => $dailyLimit, // null = 不限
|
||||
'today_sent' => (int) $todaySent,
|
||||
'daily_remaining' => $remaining, // null = 不限
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限检查:管理员是否可对目标用户执行指定操作
|
||||
*
|
||||
|
||||
@@ -59,7 +59,8 @@
|
||||
appointPositionsUrl: "{{ route('chat.appoint.positions') }}",
|
||||
appointUrl: "{{ route('chat.appoint.appoint') }}",
|
||||
revokeUrl: "{{ route('chat.appoint.revoke') }}",
|
||||
rewardUrl: "{{ route('command.reward') }}"
|
||||
rewardUrl: "{{ route('command.reward') }}",
|
||||
rewardQuotaUrl: "{{ route('command.reward_quota') }}"
|
||||
};
|
||||
</script>
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js', 'resources/js/chat.js'])
|
||||
|
||||
@@ -687,7 +687,7 @@
|
||||
<button x-show="window.chatContext?.myMaxReward !== 0"
|
||||
style="flex:1; padding: 7px 10px; border-radius: 5px; font-size: 12px; font-weight: bold; cursor: pointer;
|
||||
background: linear-gradient(135deg,#f59e0b,#d97706); color:#fff; border:none;"
|
||||
x-on:click="showRewardPanel = !showRewardPanel; showGiftPanel = false;">
|
||||
x-on:click="openRewardModal(userInfo.username)">
|
||||
🪙 送金币
|
||||
</button>
|
||||
</div>
|
||||
@@ -742,41 +742,6 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 内联奖励金币面板(仅职务持有者且有 max_reward 时可用) --}}
|
||||
<div x-show="showRewardPanel" x-transition
|
||||
style="display: none; padding: 12px 16px; background: #fffbeb; border-top: 2px solid #f59e0b;">
|
||||
|
||||
<div
|
||||
style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||
<span style="font-size: 13px; color: #92400e; font-weight: bold;">🪙 发放奖励金币</span>
|
||||
<span style="font-size: 11px; color: #b45309;">
|
||||
单次上限:<b
|
||||
x-text="window.chatContext?.myMaxReward === -1 ? '不限' : (window.chatContext?.myMaxReward ?? 0)"></b>
|
||||
<span x-show="window.chatContext?.myMaxReward !== -1"> 金币</span>
|
||||
</span>
|
||||
<button x-on:click="showRewardPanel = false"
|
||||
style="background: none; border: none; color: #94a3b8; cursor: pointer; font-size: 18px; line-height: 1;">×</button>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 8px; align-items: center;">
|
||||
<input type="number" x-model.number="rewardAmount" :min="1"
|
||||
:max="window.chatContext?.myMaxReward > 0 ? window.chatContext.myMaxReward : 999999"
|
||||
placeholder="输入金额"
|
||||
style="flex:1; height: 36px; padding: 0 10px; border: 1px solid #fcd34d;
|
||||
border-radius: 6px; font-size: 14px; color: #92400e; background: #fff;
|
||||
outline: none; box-sizing: border-box;">
|
||||
<button x-on:click="sendReward()" :disabled="sendingReward || !rewardAmount"
|
||||
style="height: 36px; padding: 0 18px; background: linear-gradient(135deg,#f59e0b,#d97706);
|
||||
color:#fff; border:none; border-radius: 6px; font-size: 13px; font-weight: bold; cursor: pointer; white-space: nowrap;"
|
||||
:style="sendingReward || !rewardAmount ? 'opacity:0.6; cursor:not-allowed;' : ''">
|
||||
<span x-text="sendingReward ? '发放中...' : '🎉 确认发放'"></span>
|
||||
</button>
|
||||
</div>
|
||||
<p style="margin: 6px 0 0; font-size: 10px; color: #b45309; opacity: 0.8;">
|
||||
金币将直接发放给对方账户,本操作记入你的履职记录。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 管理操作 + 职务操作 合并折叠区 --}}
|
||||
@@ -910,3 +875,142 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ═══════════ 奖励金币独立弹窗 ═══════════ --}}
|
||||
<div id="reward-modal-container"
|
||||
x-data="{
|
||||
show: false,
|
||||
targetUsername: '',
|
||||
amount: '',
|
||||
sending: false,
|
||||
loading: false,
|
||||
quota: { max_once: null, daily_limit: null, today_sent: 0, daily_remaining: null },
|
||||
|
||||
fmt(v) {
|
||||
if (v === null) return '不限';
|
||||
if (v === 0) return '—';
|
||||
return v.toLocaleString() + ' 金币';
|
||||
},
|
||||
|
||||
async open(username) {
|
||||
this.targetUsername = username;
|
||||
this.amount = '';
|
||||
this.sending = false;
|
||||
this.loading = true;
|
||||
this.show = true;
|
||||
try {
|
||||
const res = await fetch(window.chatContext.rewardQuotaUrl, {
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
this.quota = await res.json();
|
||||
} catch { this.quota = { max_once: null, daily_limit: null, today_sent: 0, daily_remaining: null }; }
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
async send() {
|
||||
if (this.sending) return;
|
||||
const amt = parseInt(this.amount, 10);
|
||||
if (!amt || amt <= 0) { alert('请输入有效金额'); return; }
|
||||
const maxOnce = window.chatContext?.myMaxReward;
|
||||
if (maxOnce === 0) { alert('你的职务没有奖励发放权限'); return; }
|
||||
if (maxOnce > 0 && amt > maxOnce) { alert('超出单次上限 ' + maxOnce + ' 金币'); return; }
|
||||
this.sending = true;
|
||||
try {
|
||||
const res = await fetch(window.chatContext.rewardUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]')?.content || '',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ username: this.targetUsername, room_id: window.chatContext.roomId, amount: amt }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
this.quota.today_sent += amt;
|
||||
if (this.quota.daily_remaining !== null) {
|
||||
this.quota.daily_remaining = Math.max(0, this.quota.daily_remaining - amt);
|
||||
}
|
||||
this.amount = '';
|
||||
alert(data.message);
|
||||
} else {
|
||||
alert(data.message || '发放失败');
|
||||
}
|
||||
} catch { alert('网络异常,请稍后重试'); }
|
||||
this.sending = false;
|
||||
}
|
||||
}">
|
||||
<div x-show="show" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,.55); z-index:9900;" x-on:click.self="show = false">
|
||||
<div x-show="show"
|
||||
style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
|
||||
width:320px; background:#fff; border-radius:16px;
|
||||
box-shadow:0 20px 60px rgba(0,0,0,.25); overflow:hidden;">
|
||||
<div style="background:linear-gradient(135deg,#f59e0b,#d97706); padding:14px 18px;
|
||||
display:flex; align-items:center; justify-content:space-between;">
|
||||
<div>
|
||||
<div style="color:#fff; font-weight:bold; font-size:15px;">🪙 发放奖励金币</div>
|
||||
<div style="color:rgba(255,255,255,.85); font-size:12px; margin-top:2px;" x-text="'发给:' + targetUsername"></div>
|
||||
</div>
|
||||
<button x-on:click="show = false"
|
||||
style="background:rgba(255,255,255,.2); border:none; color:#fff; width:28px; height:28px;
|
||||
border-radius:50%; cursor:pointer; font-size:16px; line-height:28px; text-align:center;">×</button>
|
||||
</div>
|
||||
<div style="padding:16px 18px 0;">
|
||||
<div x-show="loading" style="text-align:center; color:#b45309; font-size:13px; padding:12px 0;">加载额度信息…</div>
|
||||
<div x-show="!loading" style="display:grid; grid-template-columns:1fr 1fr; gap:8px;">
|
||||
<div style="background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px; text-align:center;">
|
||||
<div style="font-size:10px; color:#b45309; margin-bottom:4px;">单次上限</div>
|
||||
<div style="font-size:14px; font-weight:bold; color:#92400e;" x-text="fmt(quota.max_once)"></div>
|
||||
</div>
|
||||
<div style="background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px; text-align:center;">
|
||||
<div style="font-size:10px; color:#b45309; margin-bottom:4px;">单日上限</div>
|
||||
<div style="font-size:14px; font-weight:bold; color:#92400e;" x-text="fmt(quota.daily_limit)"></div>
|
||||
</div>
|
||||
<div style="background:#f0fdf4; border:1px solid #bbf7d0; border-radius:8px; padding:10px; text-align:center;">
|
||||
<div style="font-size:10px; color:#166534; margin-bottom:4px;">今日已发</div>
|
||||
<div style="font-size:14px; font-weight:bold; color:#15803d;" x-text="quota.today_sent.toLocaleString() + ' 金币'"></div>
|
||||
</div>
|
||||
<div style="background:#f0fdf4; border:1px solid #bbf7d0; border-radius:8px; padding:10px; text-align:center;">
|
||||
<div style="font-size:10px; color:#166534; margin-bottom:4px;">剩余额度</div>
|
||||
<div style="font-size:14px; font-weight:bold; color:#15803d;" x-text="fmt(quota.daily_remaining)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding:16px 18px 18px;">
|
||||
<div style="display:flex; gap:8px; align-items:center; margin-bottom:10px;">
|
||||
<input type="number" x-model.number="amount"
|
||||
:placeholder="quota.max_once ? '最多 ' + quota.max_once + ' 金币' : '请输入金额'"
|
||||
:max="quota.max_once || 999999" min="1"
|
||||
x-on:keydown.enter="send()"
|
||||
style="flex:1; height:40px; padding:0 12px; border:2px solid #fcd34d;
|
||||
border-radius:8px; font-size:14px; color:#92400e; outline:none; box-sizing:border-box;">
|
||||
<button x-on:click="send()" :disabled="sending || !amount"
|
||||
style="height:40px; padding:0 16px; background:linear-gradient(135deg,#f59e0b,#d97706);
|
||||
color:#fff; border:none; border-radius:8px; font-size:13px; font-weight:bold;
|
||||
cursor:pointer; white-space:nowrap; min-width:80px;"
|
||||
:style="(sending || !amount) ? 'opacity:.6; cursor:not-allowed;' : ''">
|
||||
<span x-text="sending ? '发放中…' : '🎉 确认'"></span>
|
||||
</button>
|
||||
</div>
|
||||
<p style="margin:0; font-size:10px; color:#b45309; opacity:.8; text-align:center;">
|
||||
金币凭空产生并直接发放给对方,本操作记入你的履职记录。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* openRewardModal - 全局入口:打开奖励金币独立弹窗
|
||||
* @param {string} username 被奖励用户名
|
||||
*/
|
||||
function openRewardModal(username) {
|
||||
const el = document.getElementById('reward-modal-container');
|
||||
if (el) {
|
||||
const data = Alpine.$data(el);
|
||||
if (data) data.open(username);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ Route::middleware(['chat.auth'])->group(function () {
|
||||
Route::post('/command/mute', [AdminCommandController::class, 'mute'])->name('command.mute');
|
||||
Route::post('/command/freeze', [AdminCommandController::class, 'freeze'])->name('command.freeze');
|
||||
Route::post('/command/reward', [AdminCommandController::class, 'reward'])->name('command.reward');
|
||||
Route::get('/command/reward-quota', [AdminCommandController::class, 'rewardQuota'])->name('command.reward_quota');
|
||||
Route::get('/command/whispers/{username}', [AdminCommandController::class, 'viewWhispers'])->name('command.whispers');
|
||||
Route::post('/command/announce', [AdminCommandController::class, 'announce'])->name('command.announce');
|
||||
Route::post('/command/clear-screen', [AdminCommandController::class, 'clearScreen'])->name('command.clear_screen');
|
||||
|
||||
Reference in New Issue
Block a user