- 路由: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 可新增/删除
352 lines
20 KiB
PHP
352 lines
20 KiB
PHP
{{--
|
||
文件功能:后台商店商品管理页面(站长功能)
|
||
支持查看、新增、编辑、上下架切换、删除商品。
|
||
字段:名称、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">×</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
|