Files
chatroom/resources/views/admin/shop/index.blade.php
lkddi 759fb6deae 功能:后台新增商店管理页面(站长菜单)
- 路由:GET/POST/PUT/PATCH/DELETE /admin/shop
- 控制器:Admin/ShopItemController(index/store/update/toggle/destroy)
- 视图:admin/shop/index.blade.php
  - 表格展示所有商品(名称/类型色标/价格/有效期/排序/状态)
  - Alpine.js 弹窗新增/编辑(支持全字段)
  - 上下架一键切换(PATCH toggle)
  - 删除按键(含二次确认)
- 侧边栏:VIP 下方新增「🛒 商店管理」链接
- 权限:superlevel 可查看/编辑;id=1 可新增/删除
2026-03-01 16:47:34 +08:00

352 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.
{{--
文件功能:后台商店商品管理页面(站长功能)
支持查看、新增、编辑、上下架切换、删除商品。
字段名称、Slug、图标、描述、价格、类型、有效期、排序、状态
@author ChatRoom Laravel
@version 1.0.0
--}}
@extends('admin.layouts.app')
@section('title', '🛒 商店商品管理')
@section('content')
@php
$typeLabels = [
'instant' => ['label' => '即时特效', 'color' => 'bg-blue-100 text-blue-700'],
'duration' => ['label' => '周卡/时效', 'color' => 'bg-purple-100 text-purple-700'],
'one_time' => ['label' => '一次性道具', 'color' => 'bg-yellow-100 text-yellow-700'],
'ring' => ['label' => '求婚戒指', 'color' => 'bg-rose-100 text-rose-700'],
'auto_fishing' => ['label' => '自动钓鱼卡', 'color' => 'bg-emerald-100 text-emerald-700'],
];
$isSuperAdmin = Auth::id() === 1;
@endphp
<div x-data="{
showForm: false,
editing: null,
form: {
name: '',
slug: '',
icon: '🎁',
description: '',
price: 100,
type: 'instant',
duration_days: 0,
duration_minutes: 0,
intimacy_bonus: 0,
charm_bonus: 0,
sort_order: 0,
is_active: true,
},
openCreate() {
this.editing = null;
this.form = {
name: '',
slug: '',
icon: '🎁',
description: '',
price: 100,
type: 'instant',
duration_days: 0,
duration_minutes: 0,
intimacy_bonus: 0,
charm_bonus: 0,
sort_order: 0,
is_active: true,
};
this.showForm = true;
this.$nextTick(() => this.$refs.nameInput?.focus());
},
openEdit(item) {
this.editing = item;
this.form = {
name: item.name,
slug: item.slug,
icon: item.icon,
description: item.description || '',
price: item.price,
type: item.type,
duration_days: item.duration_days || 0,
duration_minutes: item.duration_minutes || 0,
intimacy_bonus: item.intimacy_bonus || 0,
charm_bonus: item.charm_bonus || 0,
sort_order: item.sort_order,
is_active: item.is_active,
};
this.showForm = true;
this.$nextTick(() => this.$refs.nameInput?.focus());
},
closeForm() {
this.showForm = false;
this.editing = null;
}
}">
{{-- 头部操作栏 --}}
<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>
@if ($isSuperAdmin)
<button @click="openCreate()"
class="bg-indigo-600 text-white px-5 py-2.5 rounded-lg font-bold hover:bg-indigo-700 transition shadow-sm">
+ 新增商品
</button>
@endif
</div>
{{-- 商品列表表格 --}}
<div class="bg-white rounded-xl shadow overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50 text-gray-500 uppercase text-xs tracking-wider">
<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-right">价格</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>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@forelse ($items as $item)
@php $tl = $typeLabels[$item->type] ?? ['label' => $item->type, 'color' => 'bg-gray-100 text-gray-600']; @endphp
<tr class="hover:bg-gray-50 transition {{ $item->is_active ? '' : 'opacity-50' }}">
{{-- 商品信息 --}}
<td class="px-4 py-3">
<div class="flex items-center gap-3">
<span class="text-2xl leading-none">{{ $item->icon }}</span>
<div>
<p class="font-semibold text-gray-800">{{ $item->name }}</p>
<p class="text-xs text-gray-400 font-mono">{{ $item->slug }}</p>
@if ($item->description)
<p class="text-xs text-gray-500 mt-0.5 max-w-xs truncate">
{{ $item->description }}</p>
@endif
</div>
</div>
</td>
{{-- 类型 --}}
<td class="px-4 py-3">
<span class="px-2 py-0.5 rounded-full text-xs font-semibold {{ $tl['color'] }}">
{{ $tl['label'] }}
</span>
</td>
{{-- 价格 --}}
<td class="px-4 py-3 text-right font-mono font-bold text-amber-600">
{{ number_format($item->price) }}
</td>
{{-- 有效期 --}}
<td class="px-4 py-3 text-center text-gray-500 text-xs">
@if ($item->duration_minutes > 0)
{{ $item->duration_minutes >= 60 ? floor($item->duration_minutes / 60) . '小时' : $item->duration_minutes . '分钟' }}
@elseif ($item->duration_days > 0)
{{ $item->duration_days }}
@else
@endif
</td>
{{-- 排序 --}}
<td class="px-4 py-3 text-center text-gray-400 font-mono text-xs">{{ $item->sort_order }}</td>
{{-- 状态 --}}
<td class="px-4 py-3 text-center">
<form method="POST" action="{{ route('admin.shop.toggle', $item) }}" class="inline">
@csrf @method('PATCH')
<button type="submit"
class="px-2.5 py-1 rounded-full text-xs font-bold transition
{{ $item->is_active
? 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200'
: 'bg-gray-100 text-gray-500 hover:bg-gray-200' }}">
{{ $item->is_active ? '上架中' : '已下架' }}
</button>
</form>
</td>
{{-- 操作 --}}
<td class="px-4 py-3">
<div class="flex items-center justify-center gap-2">
<button
@click="openEdit({{ json_encode([
'id' => $item->id,
'name' => $item->name,
'slug' => $item->slug,
'icon' => $item->icon,
'description' => $item->description,
'price' => $item->price,
'type' => $item->type,
'duration_days' => $item->duration_days,
'duration_minutes' => $item->duration_minutes,
'intimacy_bonus' => $item->intimacy_bonus,
'charm_bonus' => $item->charm_bonus,
'sort_order' => $item->sort_order,
'is_active' => (bool) $item->is_active,
]) }})"
class="text-indigo-600 hover:text-indigo-800 text-xs font-semibold px-2 py-1 rounded hover:bg-indigo-50 transition">
编辑
</button>
@if ($isSuperAdmin)
<form method="POST" action="{{ route('admin.shop.destroy', $item) }}"
onsubmit="return confirm('确定要删除「{{ $item->name }}」吗?此操作不可撤销!')">
@csrf @method('DELETE')
<button type="submit"
class="text-red-500 hover:text-red-700 text-xs font-semibold px-2 py-1 rounded hover:bg-red-50 transition">
删除
</button>
</form>
@endif
</div>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="px-4 py-12 text-center text-gray-400">暂无商品数据</td>
</tr>
@endforelse
</tbody>
</table>
</div>
{{-- 新增/编辑 抽屉弹窗 --}}
<div x-show="showForm" x-cloak class="fixed inset-0 z-50 flex items-center justify-center"
style="background: rgba(0,0,0,0.45);">
<div @click.stop class="bg-white rounded-2xl shadow-2xl w-full max-w-xl max-h-[90vh] overflow-y-auto"
x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 translate-y-4"
x-transition:enter-end="opacity-100 translate-y-0">
{{-- 弹窗头部 --}}
<div class="flex items-center justify-between px-6 py-4 border-b">
<h3 class="font-bold text-gray-800 text-lg" x-text="editing ? '编辑商品:' + editing.name : '新增商品'"></h3>
<button @click="closeForm()"
class="text-gray-400 hover:text-gray-600 text-2xl leading-none">&times;</button>
</div>
{{-- 表单 --}}
<form method="POST"
:action="editing
?
'{{ url('admin/shop') }}/' + editing.id :
'{{ route('admin.shop.store') }}'"
class="px-6 py-5 space-y-4">
@csrf
<template x-if="editing"><input type="hidden" name="_method" value="PUT"></template>
{{-- 基本信息 --}}
<div class="grid grid-cols-2 gap-4">
<div class="col-span-2">
<label class="block text-xs font-semibold text-gray-600 mb-1">商品名称 <span
class="text-red-500">*</span></label>
<input x-ref="nameInput" type="text" name="name" x-model="form.name" required
maxlength="100"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-400 focus:border-indigo-400 outline-none">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Slug唯一标识<span
class="text-red-500">*</span></label>
<input type="text" name="slug" x-model="form.slug" required maxlength="100"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm font-mono focus:ring-2 focus:ring-indigo-400 focus:border-indigo-400 outline-none">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">图标Emoji<span
class="text-red-500">*</span></label>
<input type="text" name="icon" x-model="form.icon" required maxlength="20"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-400 focus:border-indigo-400 outline-none">
</div>
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">商品描述</label>
<textarea name="description" x-model="form.description" rows="2" maxlength="500"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-400 focus:border-indigo-400 outline-none resize-none"></textarea>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">价格(金币)<span
class="text-red-500">*</span></label>
<input type="number" name="price" x-model="form.price" required min="0"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-400 focus:border-indigo-400 outline-none">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">商品类型 <span
class="text-red-500">*</span></label>
<select name="type" x-model="form.type" required
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-400 focus:border-indigo-400 outline-none bg-white">
<option value="instant">instant 即时特效</option>
<option value="duration">duration 周卡/时效</option>
<option value="one_time">one_time 一次性道具</option>
<option value="ring">ring 求婚戒指</option>
<option value="auto_fishing">auto_fishing 自动钓鱼卡</option>
</select>
</div>
</div>
<div class="grid grid-cols-3 gap-4">
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">有效天数</label>
<input type="number" name="duration_days" x-model="form.duration_days" min="0"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-400 focus:border-indigo-400 outline-none">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">有效分钟数</label>
<input type="number" name="duration_minutes" x-model="form.duration_minutes" min="0"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-400 focus:border-indigo-400 outline-none">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">排序权重</label>
<input type="number" name="sort_order" x-model="form.sort_order" min="0"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-400 focus:border-indigo-400 outline-none">
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">亲密度加成(戒指)</label>
<input type="number" name="intimacy_bonus" x-model="form.intimacy_bonus" min="0"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-400 focus:border-indigo-400 outline-none">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">魅力值加成(戒指)</label>
<input type="number" name="charm_bonus" x-model="form.charm_bonus" min="0"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-400 focus:border-indigo-400 outline-none">
</div>
</div>
<div class="flex items-center gap-2">
<input type="hidden" name="is_active" :value="form.is_active ? 1 : 0">
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="form.is_active" class="sr-only peer">
<div
class="w-10 h-5 bg-gray-200 peer-checked:bg-indigo-500 rounded-full transition peer-focus:ring-2 peer-focus:ring-indigo-300">
</div>
<div
class="absolute left-0.5 top-0.5 w-4 h-4 bg-white rounded-full shadow transition peer-checked:translate-x-5">
</div>
</label>
<span class="text-sm text-gray-600" x-text="form.is_active ? '🟢 上架显示' : '⚫ 下架隐藏'"></span>
</div>
{{-- 弹窗底部按钮 --}}
<div class="flex justify-end gap-3 pt-2 border-t mt-4">
<button type="button" @click="closeForm()"
class="px-5 py-2 rounded-lg border border-gray-300 text-gray-600 hover:bg-gray-50 transition text-sm">
取消
</button>
<button type="submit"
class="px-6 py-2 bg-indigo-600 text-white rounded-lg font-bold hover:bg-indigo-700 transition text-sm shadow">
<span x-text="editing ? '保存修改' : '创建商品'"></span>
</button>
</div>
</form>
</div>
</div>
</div>
@endsection