fix(Phase 2): 修复后台路由+视图路径,Vrticket控制器上线
- 新增 Vrticket.php(ThinkPHP小写路由约定,类名Vrticket)
- 修复视图标签:{include} → {{include}}(ShopXO使用{{}}分隔符)
- 视图文件同步到 app/admin/view/default/plugins/view/vr_ticket/
- 插件控制器类名 Seattemplate(ThinkPHP路由适配)
- plugin.json 新增场馆配置菜单项
- 删除废弃的 plugins/index.html 符号链接
- 自动载入:app.php 注册 app\plugins\ 命名空间映射
refactor/vr-ticket-20260416
parent
f1f061a8d7
commit
5749edf6ad
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\admin\controller\Common;
|
||||
|
||||
/**
|
||||
* VR票务后台管理控制器
|
||||
* @author Sileya
|
||||
*/
|
||||
class Vrticket extends Common
|
||||
{
|
||||
/**
|
||||
* 构造方法
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* 座位模板列表
|
||||
*/
|
||||
public function SeatTemplateList()
|
||||
{
|
||||
return $this->render('seat_template/list');
|
||||
}
|
||||
|
||||
/**
|
||||
* 座位模板保存
|
||||
*/
|
||||
public function SeatTemplateSave()
|
||||
{
|
||||
return $this->render('seat_template/save');
|
||||
}
|
||||
|
||||
/**
|
||||
* 电子票列表
|
||||
*/
|
||||
public function TicketList()
|
||||
{
|
||||
return $this->render('ticket/list');
|
||||
}
|
||||
|
||||
/**
|
||||
* 电子票详情
|
||||
*/
|
||||
public function TicketDetail()
|
||||
{
|
||||
return $this->render('ticket/detail');
|
||||
}
|
||||
|
||||
/**
|
||||
* 核销员列表
|
||||
*/
|
||||
public function VerifierList()
|
||||
{
|
||||
return $this->render('verifier/list');
|
||||
}
|
||||
|
||||
/**
|
||||
* 核销员保存
|
||||
*/
|
||||
public function VerifierSave()
|
||||
{
|
||||
return $this->render('verifier/save');
|
||||
}
|
||||
|
||||
/**
|
||||
* 核销记录列表
|
||||
*/
|
||||
public function VerificationList()
|
||||
{
|
||||
return $this->render('verification/list');
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染插件视图
|
||||
* @param string $template 模板路径(相对于 vr_ticket/admin/view/ 目录)
|
||||
*/
|
||||
protected function render($template)
|
||||
{
|
||||
// 插件视图路径(从 app/admin/view/default/plugins/view/vr_ticket/admin/view/ 开始)
|
||||
return MyView('plugins/view/vr_ticket/admin/view/' . $template);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
{{:ModuleInclude('public/header')}}
|
||||
|
||||
<!-- right content start -->
|
||||
<div class="content-right">
|
||||
<div class="content">
|
||||
<div class="table-no">
|
||||
<div>
|
||||
<i class="am-icon-skyatlas am-icon-lg"></i>
|
||||
<p>{{if empty($msg)}}{{:MyLang('no_data')}}{{else /}}{{$msg}}{{/if}}</p>
|
||||
</div>
|
||||
<div class="am-margin-top-lg">
|
||||
<a href="{{:MyUrl('admin/pluginsadmin/index')}}" class="am-btn am-btn-link am-radius am-btn-xs">{{:MyLang('plugins.back_to_plugins_admin')}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- right content end -->
|
||||
|
||||
<!-- footer start -->
|
||||
{{:ModuleInclude('public/footer')}}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>座位模板 - VR票务</title>
|
||||
{{include file="public/head" }}
|
||||
</head>
|
||||
<body>
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">座位模板管理</div>
|
||||
<div class="layui-card-body">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="layui-form layui-form-pane">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">模板名称</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="name" value="" placeholder="搜索模板名称" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">状态</label>
|
||||
<div class="layui-input-inline">
|
||||
<select name="status" lay-search>
|
||||
<option value="">全部</option>
|
||||
<option value="1">启用</option>
|
||||
<option value="0">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button class="layui-btn" lay-submit lay-filter="search">搜索</button>
|
||||
<a href="{:PluginsAdminUrl('vr_ticket', 'seat_template', 'save')}" class="layui-btn layui-btn-normal">添加模板</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<table class="layui-hide" id="table" lay-filter="table"></table>
|
||||
|
||||
<script type="text/template" id="statusTpl">
|
||||
{{# if (d.status == 1) { }}
|
||||
<span class="layui-badge layui-bg-green">启用</span>
|
||||
{{# } else { }}
|
||||
<span class="layui-badge layui-bg-gray">禁用</span>
|
||||
{{# } }}
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="actionTpl">
|
||||
<a href="{:PluginsAdminUrl('vr_ticket', 'seat_template', 'save')}?id={{d.id}}" class="layui-btn layui-btn-xs">编辑</a>
|
||||
<a href="javascript:;" class="layui-btn layui-btn-danger layui-btn-xs" lay-fn="del" data-id="{{d.id}}">删除</a>
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{include file="public/footer" }}
|
||||
<script>
|
||||
layui.use('table', function() {
|
||||
var table = layui.table;
|
||||
var form = layui.form;
|
||||
|
||||
table.render({
|
||||
elem: '#table',
|
||||
url: '{:PluginsAdminUrl("vr_ticket", "seat_template", "list")}',
|
||||
cols: [[
|
||||
{field: 'id', title: 'ID', width: 80},
|
||||
{field: 'name', title: '模板名称', minWidth: 150},
|
||||
{field: 'category_name', title: '绑定分类', width: 150},
|
||||
{field: 'seat_count', title: '座位数', width: 100},
|
||||
{field: 'status', title: '状态', width: 100, templet: '#statusTpl'},
|
||||
{field: 'add_time', title: '创建时间', width: 180, templet: function(d) {
|
||||
return d.add_time > 0 ? layui.util.toDateString(d.add_time * 1000) : '-';
|
||||
}},
|
||||
{field: 'action', title: '操作', width: 150, templet: '#actionTpl'},
|
||||
]]
|
||||
});
|
||||
|
||||
form.on('submit(search)', function(data) {
|
||||
table.reload('table', {where: data.field, page: {curr: 1}});
|
||||
return false;
|
||||
});
|
||||
|
||||
$(document).on('click', '[lay-fn="del"]', function() {
|
||||
var id = $(this).data('id');
|
||||
layer.confirm('确认删除?', function(index) {
|
||||
$.post('{:PluginsAdminUrl("vr_ticket", "seat_template", "delete")}', {id: id}, function(res) {
|
||||
if (res.code == 0) {
|
||||
layer.msg('删除成功');
|
||||
table.reload('table');
|
||||
} else {
|
||||
layer.msg(res.msg || '删除失败');
|
||||
}}
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{$info ? '编辑' : '添加'}座位模板 - VR票务</title>
|
||||
{{include file="public/head" }}
|
||||
</head>
|
||||
<body>
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">{$info ? '编辑' : '添加'}座位模板</div>
|
||||
<div class="layui-card-body">
|
||||
<form class="layui-form" lay-filter="form">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">模板名称</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="name" value="{$info.name|default=''}" required lay-verify="required" placeholder="如:鸟巢-A区" class="layui-input" style="width:400px">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">绑定分类</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="category_id" lay-search required lay-verify="required">
|
||||
<option value="">请选择分类</option>
|
||||
{foreach $categories as $cat}
|
||||
<option value="{$cat.id}" {if isset($info.category_id) && $info.category_id == $cat.id}selected{/if}>{$cat.name}</option>
|
||||
{/foreach}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">状态</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="checkbox" name="status" value="1" lay-skin="switch" lay-text="启用|禁用" {if !isset($info.status) || $info.status == 1}checked{/if}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item layui-form-text">
|
||||
<label class="layui-form-label">座位地图JSON</label>
|
||||
<div class="layui-input-block">
|
||||
<textarea name="seat_map" rows="10" placeholder="座位地图配置JSON,参考ShopXO插件文档" class="layui-textarea" style="width:600px">{$info.seat_map|default=''|raw}</textarea>
|
||||
</div>
|
||||
<div class="layui-form-mid layui-word-aux">
|
||||
格式:{"map":["AAAAAA","BBBBB"],"seats":{"A":{"price":599,"label":"VIP"},"B":{"price":299,"label":"普通"}},"sections":[]}
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item layui-form-text">
|
||||
<label class="layui-form-label">规格映射JSON</label>
|
||||
<div class="layui-input-block">
|
||||
<textarea name="spec_base_id_map" rows="4" placeholder="座位ID到spec_base_id的映射,格式:{"A":123,"B":124}" class="layui-textarea" style="width:600px">{$info.spec_base_id_map|default=''|raw}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<input type="hidden" name="id" value="{$info.id|default=0}">
|
||||
<button class="layui-btn" lay-submit lay-filter="submit">保存</button>
|
||||
<a href="{:PluginsAdminUrl('vr_ticket', 'seat_template', 'list')}" class="layui-btn layui-btn-primary">返回</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{include file="public/footer" }}
|
||||
<script>
|
||||
layui.use('form', function() {
|
||||
var form = layui.form;
|
||||
form.on('submit(submit)', function(data) {
|
||||
$.post(window.location.href, data.field, function(res) {
|
||||
if (res.code == 0) {
|
||||
layer.msg(res.msg || '保存成功', function() {
|
||||
if (res.data && res.data.url) {
|
||||
location.href = res.data.url;
|
||||
}}
|
||||
});
|
||||
} else {
|
||||
layer.msg(res.msg || '保存失败');
|
||||
}}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>票详情 - VR票务</title>
|
||||
{{include file="public/head" }}
|
||||
<style>
|
||||
.ticket-detail-card {max-width:800px;}
|
||||
.detail-row {padding:12px 0;border-bottom:1px solid #f0f0f0;}
|
||||
.detail-label {color:#999;font-size:13px;margin-bottom:4px;}
|
||||
.detail-value {font-size:14px;}
|
||||
.qr-box {text-align:center;padding:20px;background:#fafafa;border-radius:4px;}
|
||||
.qr-box img {max-width:200px;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card ticket-detail-card">
|
||||
<div class="layui-card-header">
|
||||
票详情
|
||||
<span class="layui-badge layui-bg-{{$ticket['verify_status']==1 ? 'green' : ($ticket['verify_status']==2 ? 'red' : 'blue')}}">
|
||||
{{$ticket['verify_status']==1 ? '已核销' : ($ticket['verify_status']==2 ? '已退款' : '未核销')}}
|
||||
</span>
|
||||
<span class="layui-btn layui-btn-xs layui-btn-primary" style="float:right" onclick="history.back()">返回</span>
|
||||
</div>
|
||||
<div class="layui-card-body">
|
||||
<!-- 票码 -->
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">票码</div>
|
||||
<div class="detail-value" style="font-family:monospace;font-size:16px;color:#1e9fff">{$ticket.ticket_code}</div>
|
||||
</div>
|
||||
|
||||
<!-- 二维码 -->
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">二维码</div>
|
||||
<div class="qr-box">
|
||||
<img src="{$ticket.qr_code_url}" alt="票二维码">
|
||||
<div style="margin-top:10px;color:#999;font-size:12px">扫描核销</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">关联商品</div>
|
||||
<div class="detail-value">{$goods['title']|default='已删除商品'}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">订单号</div>
|
||||
<div class="detail-value">{$ticket.order_no}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">座位信息</div>
|
||||
<div class="detail-value">{$ticket.seat_info|default='无'}</div>
|
||||
</div>
|
||||
|
||||
<!-- 观演人 -->
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">观演人</div>
|
||||
<div class="detail-value">{$ticket.real_name|default='-'}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">手机号</div>
|
||||
<div class="detail-value">{$ticket.phone|default='-'}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">身份证</div>
|
||||
<div class="detail-value">{$ticket.id_card|default='-'}</div>
|
||||
</div>
|
||||
|
||||
<!-- 时间 -->
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">发放时间</div>
|
||||
<div class="detail-value">{$ticket.issued_at > 0 ? date('Y-m-d H:i:s', $ticket.issued_at) : '-'}</div>
|
||||
</div>
|
||||
{if $ticket['verify_status'] == 1}
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">核销时间</div>
|
||||
<div class="detail-value">{$ticket.verify_time > 0 ? date('Y-m-d H:i:s', $ticket.verify_time) : '-'}</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">核销员</div>
|
||||
<div class="detail-value">{$verifier['name']|default='-'}</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- 操作 -->
|
||||
{if $ticket['verify_status'] == 0}
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">手动核销</div>
|
||||
<div class="detail-value">
|
||||
<form class="layui-form" style="display:inline" id="verify-form">
|
||||
<select name="verifier_id" required lay-verify="required" style="width:200px;display:inline">
|
||||
<option value="">选择核销员</option>
|
||||
{foreach $verifiers as $v}
|
||||
<option value="{$v.id}">{$v.name}</option>
|
||||
{/foreach}
|
||||
</select>
|
||||
<button class="layui-btn layui-btn-sm layui-btn-danger" lay-submit lay-filter="do-verify">确认核销</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{include file="public/footer" }}
|
||||
<script>
|
||||
layui.use('form', function() {
|
||||
var form = layui.form;
|
||||
form.on('submit(do-verify)', function(data) {
|
||||
if (!data.field.verifier_id) {
|
||||
layer.msg('请选择核销员');
|
||||
return false;
|
||||
}}
|
||||
$.post('{:MyUrl("plugins_vr_ticket/admin/ticket/verify")}', {
|
||||
ticket_code: '{$ticket.ticket_code}',
|
||||
verifier_id: data.field.verifier_id
|
||||
}, function(res) {
|
||||
if (res.code == 0) {
|
||||
layer.msg('核销成功', function() { location.reload(); });
|
||||
} else {
|
||||
layer.msg(res.msg || '核销失败');
|
||||
}}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>电子票管理 - VR票务</title>
|
||||
{{include file="public/head" }}
|
||||
</head>
|
||||
<body>
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">电子票管理</div>
|
||||
<div class="layui-card-body">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="layui-form layui-form-pane">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">关键词</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="keywords" value="" placeholder="订单号/票码/姓名/手机" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">核销状态</label>
|
||||
<div class="layui-input-inline">
|
||||
<select name="verify_status">
|
||||
<option value="">全部</option>
|
||||
<option value="0">未核销</option>
|
||||
<option value="1">已核销</option>
|
||||
<option value="2">已退款</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button class="layui-btn" lay-submit lay-filter="search">搜索</button>
|
||||
<button type="button" class="layui-btn layui-btn-primary" id="export-btn">导出CSV</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="layui-hide" id="table" lay-filter="table"></table>
|
||||
|
||||
<script type="text/template" id="statusTpl">
|
||||
{{# var colors = ['', 'green', 'red']; var texts = ['未核销', '已核销', '已退款']; }}
|
||||
<span class="layui-badge layui-bg-{{colors[d.verify_status] || 'gray'}}">{{texts[d.verify_status] || '未知'}}</span>
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="qrTpl">
|
||||
{{# if (d.qr_code_url) { }}
|
||||
<img src="{{d.qr_code_url}}" style="width:50px;height:50px;cursor:pointer" lay-fn="preview" data-src="{{d.qr_code_url}}">
|
||||
{{# } else { }}
|
||||
-
|
||||
{{# } }}
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="actionTpl">
|
||||
<a href="{:PluginsAdminUrl('vr_ticket', 'ticket', 'detail')}?id={{d.id}}" class="layui-btn layui-btn-xs">详情</a>
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{include file="public/footer" }}
|
||||
<script>
|
||||
layui.use(['table', 'form'], function() {
|
||||
var table = layui.table;
|
||||
var form = layui.form;
|
||||
|
||||
table.render({
|
||||
elem: '#table',
|
||||
url: '{:PluginsAdminUrl("vr_ticket", "ticket", "list")}',
|
||||
cols: [[
|
||||
{field: 'id', title: 'ID', width: 70},
|
||||
{field: 'ticket_code', title: '票码', width: 200},
|
||||
{field: 'goods_title', title: '商品', minWidth: 150},
|
||||
{field: 'real_name', title: '观演人', width: 100},
|
||||
{field: 'phone', title: '手机', width: 120},
|
||||
{field: 'seat_info', title: '座位', minWidth: 120},
|
||||
{field: 'verify_status', title: '状态', width: 100, templet: '#statusTpl'},
|
||||
{field: 'qr', title: 'QR码', width: 80, templet: '#qrTpl'},
|
||||
{field: 'issued_at', title: '发放时间', width: 160, templet: function(d) {
|
||||
return d.issued_at > 0 ? layui.util.toDateString(d.issued_at * 1000) : '-';
|
||||
}},
|
||||
{field: 'action', title: '操作', width: 80, templet: '#actionTpl'},
|
||||
]]
|
||||
});
|
||||
|
||||
form.on('submit(search)', function(data) {
|
||||
table.reload('table', {where: data.field, page: {curr: 1}});
|
||||
return false;
|
||||
});
|
||||
|
||||
// 导出 CSV:POST 触发(当前全量导出,不携带搜索条件)
|
||||
$('#export-btn').on('click', function() {
|
||||
var $form = $('<form action="{:PluginsAdminUrl(\'vr_ticket\', \'ticket\', \'export\')}" method="post" target="_blank" style="display:none"></form>');
|
||||
$(document.body).append($form);
|
||||
$form.submit().remove();
|
||||
layer.msg('正在导出,请稍候…');
|
||||
});
|
||||
|
||||
$(document).on('click', '[lay-fn="preview"]', function() {
|
||||
var src = $(this).data('src');
|
||||
layer.open({type: 1, title: 'QR码', content: '<img src="'+src+'" style="padding:20px">', area: ['300px', '350px']});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<include file="public::head" />
|
||||
<style>
|
||||
.venue-preview-img { width: 60px; height: 40px; object-fit: cover; border-radius: 4px;}}
|
||||
.zone-badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 12px; margin: 2px;}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">
|
||||
<span>场馆配置管理</span>
|
||||
<a href="{:MyUrl('plugins_vr_ticket/admin/venue/save')}" class="layui-btn layui-btn-sm layui-btn-normal fr">
|
||||
<i class="layui-icon layui-icon-add-1"></i> 添加场馆
|
||||
</a>
|
||||
</div>
|
||||
<div class="layui-card-body">
|
||||
<!-- 搜索栏 -->
|
||||
<form class="layui-form layui-form-pane" method="get">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">场馆名称</label>
|
||||
<div class="layui-input-inline" style="width:200px;">
|
||||
<input type="text" name="name" value="{:input('name')}" placeholder="搜索场馆名称" class="layui-input" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">状态</label>
|
||||
<div class="layui-input-inline" style="width:120px;">
|
||||
<select name="status" lay-search="">
|
||||
<option value="">全部</option>
|
||||
<option value="1" {:input('status')=== '1' ? 'selected' : ''}>启用</option>
|
||||
<option value="0" {:input('status')=== '0' ? 'selected' : ''}>禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button type="submit" class="layui-btn layui-btn-primary"><i class="layui-icon layui-icon-search"></i> 搜索</button>
|
||||
<a href="{:MyUrl('plugins_vr_ticket/admin/venue/list')}" class="layui-btn layui-btn-primary">重置</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<table class="layui-table" lay-skin="line">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>场馆名称</th>
|
||||
<th>场馆地址</th>
|
||||
<th>分区数</th>
|
||||
<th>座位数</th>
|
||||
<th>绑定分类</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<notempty name="list">
|
||||
<foreach name="list" item="vo">
|
||||
<tr>
|
||||
<td>{$vo.id}</td>
|
||||
<td>{$vo.venue_name}</td>
|
||||
<td class="layui-elip" style="max-width:200px;">{$vo.venue_address|default='—'}</td>
|
||||
<td><span class="layui-badge">{$vo.zone_count}</span></td>
|
||||
<td><span class="layui-badge layui-bg-blue">{$vo.seat_count}</span></td>
|
||||
<td>{$vo.category_name|default='—'}</td>
|
||||
<td>
|
||||
<eq name="vo.status" value="1">
|
||||
<span class="layui-badge layui-bg-green">启用</span>
|
||||
<else/>
|
||||
<span class="layui-badge layui-bg-gray">禁用</span>
|
||||
</eq>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{:MyUrl('plugins_vr_ticket/admin/venue/save', ['id'=>$vo['id']])}" class="layui-btn layui-btn-xs">编辑</a>
|
||||
<a href="{:MyUrl('plugins_vr_ticket/admin/seat_template/save', ['id'=>$vo['id']])}" class="layui-btn layui-btn-xs layui-btn-primary">座位模板</a>
|
||||
<a class="layui-btn layui-btn-xs layui-btn-danger js-delete" data-id="{$vo.id}">删除</a>
|
||||
</td>
|
||||
</tr>
|
||||
</foreach>
|
||||
<else/>
|
||||
<tr><td colspan="8" class="layui-text-center">暂无数据</td></tr>
|
||||
</notempty>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div>{$page|raw}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<include file="public::foot" />
|
||||
<script>
|
||||
$('.js-delete').on('click', function() {
|
||||
var id = $(this).data('id');
|
||||
layer.confirm('确认删除该场馆?', function(index) {
|
||||
$.post('{:MyUrl("plugins_vr_ticket/admin/venue/delete")}', {id: id}, function(res) {
|
||||
if (res.code === 0) {
|
||||
layer.msg(res.msg, {icon: 1});
|
||||
location.reload();
|
||||
} else {
|
||||
layer.msg(res.msg, {icon: 2});
|
||||
}}
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,453 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{if isset($info['id'])}编辑{else}添加{/if}场馆</title>
|
||||
<link rel="stylesheet" href="__STATIC__/layui/css/layui.css">
|
||||
<style>
|
||||
[v-cloak] { display: none;}}
|
||||
.venue-editor { margin-top: 20px;}}
|
||||
.venue-editor .section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin: 20px 0 10px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 2px solid #009688;
|
||||
color: #009688;
|
||||
}}
|
||||
/* 座位预览 */
|
||||
.seat-preview-wrap {
|
||||
margin: 15px 0;
|
||||
padding: 15px;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #e2e2e2;
|
||||
border-radius: 4px;
|
||||
min-height: 80px;
|
||||
}}
|
||||
.seat-preview-wrap .preview-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin-bottom: 6px;
|
||||
}}
|
||||
.seat-preview-wrap .preview-row:last-child { margin-bottom: 0;}}
|
||||
.seat-preview-wrap .seat-cell {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
border-radius: 3px;
|
||||
cursor: default;
|
||||
position: relative;
|
||||
transition: transform 0.1s;
|
||||
}}
|
||||
.seat-preview-wrap .seat-cell:hover {
|
||||
transform: scale(1.15);
|
||||
z-index: 10;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||
}}
|
||||
.seat-preview-wrap .seat-cell .seat-tooltip {
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: 38px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0,0,0,0.85);
|
||||
color: #fff;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
font-size: 11px;
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
}}
|
||||
.seat-preview-wrap .seat-cell:hover .seat-tooltip { display: block;}}
|
||||
.seat-preview-wrap .seat-cell .seat-tooltip::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 4px solid transparent;
|
||||
border-top-color: rgba(0,0,0,0.85);
|
||||
}}
|
||||
.seat-preview-stats {
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}}
|
||||
.seat-preview-stats span {
|
||||
margin-right: 16px;
|
||||
}}
|
||||
/* 分区配置 */
|
||||
.zone-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
padding: 8px;
|
||||
background: #fafafa;
|
||||
border: 1px solid #e2e2e2;
|
||||
border-radius: 4px;
|
||||
}}
|
||||
.zone-row .zone-char { width: 60px;}}
|
||||
.zone-row .zone-name { flex: 1;}}
|
||||
.zone-row .zone-price { width: 100px;}}
|
||||
.zone-row .zone-color { width: 50px; height: 36px; padding: 2px; border: 1px solid #ccc; border-radius: 3px; cursor: pointer;}}
|
||||
/* 座位排布 */
|
||||
.seat-map-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}}
|
||||
.seat-map-row label {
|
||||
width: 70px;
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
}}
|
||||
.seat-map-row input[type="text"] {
|
||||
flex: 1;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
font-size: 13px;
|
||||
font-family: monospace;
|
||||
letter-spacing: 2px;
|
||||
}}
|
||||
/* 颜色预设 */
|
||||
.color-presets {
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}}
|
||||
.color-presets .preset-swatch {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #ccc;
|
||||
transition: transform 0.1s;
|
||||
}}
|
||||
.color-presets .preset-swatch:hover { transform: scale(1.2);}}
|
||||
/* 工具栏按钮 */
|
||||
.toolbar { margin: 10px 0;}}
|
||||
/* 隐藏字段同步占位提示 */
|
||||
.sync-hint {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="layui-fluid" style="padding:20px;">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">
|
||||
{if isset($info['id'])}编辑{else}添加{/if}场馆
|
||||
</div>
|
||||
<div class="layui-card-body">
|
||||
<form class="layui-form" method="POST" lay-filter="venue-form">
|
||||
|
||||
<!-- 场馆基本信息 -->
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">场馆名称</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="name" value="{$info.name|default=''}" class="layui-input"
|
||||
lay-verify="required" placeholder="请输入场馆名称">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================
|
||||
Vue3 交互式场馆编辑器
|
||||
============================================================ -->
|
||||
<div id="venue-editor" v-cloak>
|
||||
|
||||
<div class="section-title">票务配置</div>
|
||||
|
||||
<!-- 场馆票务名 & 地址 -->
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">场馆名(票务用)</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" v-model="venue.name" class="layui-input" placeholder="票务系统展示的场馆名称">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">场馆地址</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" v-model="venue.address" class="layui-input" placeholder="场馆详细地址">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">场馆图片</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" v-model="venue.image" class="layui-input" placeholder="场馆图片URL(可选)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================
|
||||
实时座位预览
|
||||
============================================================ -->
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">座位预览</label>
|
||||
<div class="layui-input-block">
|
||||
<div class="seat-preview-wrap">
|
||||
<template v-for="(rowStr, rowIdx) in seatMapRows" :key="rowIdx">
|
||||
<div class="preview-row" v-if="rowStr.trim() !== ''">
|
||||
<template v-for="(ch, colIdx) in rowStr.trim()" :key="rowIdx + '_' + colIdx">
|
||||
<div class="seat-cell"
|
||||
:style="{ backgroundColor: getZoneColor(ch) }"
|
||||
:title="getSeatTooltip(rowIdx, colIdx, ch)">
|
||||
{{ ch }}{{ colIdx + 1 }}
|
||||
<div class="seat-tooltip">{{ getSeatTooltip(rowIdx, colIdx, ch) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="totalSeats === 0" style="color:#999;font-size:13px;text-align:center;padding:10px 0;">
|
||||
暂无座位,请添加分区和排布
|
||||
</div>
|
||||
<div class="seat-preview-stats" v-if="totalSeats > 0">
|
||||
<span>总座位数:<strong>{{ totalSeats }}</strong></span>
|
||||
<span>总排数:<strong>{{ seatMapRows.filter(r => r.trim()).length }}</strong></span>
|
||||
<span>分区数:<strong>{{ activeZones.length }}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================
|
||||
分区配置
|
||||
============================================================ -->
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">分区配置</label>
|
||||
<div class="layui-input-block">
|
||||
<div v-for="(zone, idx) in zones" :key="idx" class="zone-row">
|
||||
<input type="text" v-model="zone.char" maxlength="1"
|
||||
class="layui-input zone-char"
|
||||
placeholder="字符"
|
||||
@input="onZoneChange">
|
||||
<input type="text" v-model="zone.name"
|
||||
class="layui-input zone-name"
|
||||
placeholder="分区名称,如 VIP区">
|
||||
<input type="number" v-model="zone.price"
|
||||
class="layui-input zone-price"
|
||||
placeholder="价格" min="0">
|
||||
<input type="color" v-model="zone.color"
|
||||
class="zone-color"
|
||||
@input="onZoneChange">
|
||||
<button type="button" class="layui-btn layui-btn-xs layui-btn-danger"
|
||||
@click="removeZone(idx)">删除</button>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<button type="button" class="layui-btn layui-btn-sm" @click="addZone">
|
||||
<i class="layui-icon layui-icon-add-1"></i> 添加分区
|
||||
</button>
|
||||
</div>
|
||||
<div class="color-presets">
|
||||
<span style="font-size:12px;color:#888;margin-right:4px;">预设色:</span>
|
||||
<template v-for="c in colorPresets" :key="c">
|
||||
<div class="preset-swatch"
|
||||
:style="{ backgroundColor: c }"
|
||||
:title="c"
|
||||
@click="applyPresetColor(c)"></div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================
|
||||
座位排布
|
||||
============================================================ -->
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">座位排布</label>
|
||||
<div class="layui-input-block">
|
||||
<div v-for="(row, idx) in seatMapRows" :key="idx" class="seat-map-row">
|
||||
<label>第 {{ idx + 1 }} 排:</label>
|
||||
<input type="text"
|
||||
:value="row"
|
||||
@input="updateSeatMapRow(idx, $event)"
|
||||
placeholder="输入座位字符,如 AAAAAA">
|
||||
<button type="button" class="layui-btn layui-btn-xs layui-btn-danger"
|
||||
@click="removeSeatRow(idx)">删除</button>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<button type="button" class="layui-btn layui-btn-sm" @click="addSeatRow">
|
||||
<i class="layui-icon layui-icon-add-1"></i> 添加排
|
||||
</button>
|
||||
<span class="sync-hint">每排字符对应上方分区,例:ABABAB 表示交替座位</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- / Vue3 编辑器 -->
|
||||
|
||||
<!-- 隐藏字段:提交给 PHP -->
|
||||
<input type="hidden" name="zones" :value="zonesJson">
|
||||
<input type="hidden" name="seat_map_rows" :value="seatMapRowsJson">
|
||||
<input type="hidden" name="venue_json" :value="venueJson">
|
||||
|
||||
<div class="layui-form-item" style="margin-top:20px;">
|
||||
<div class="layui-input-block">
|
||||
<button type="submit" class="layui-btn" lay-submit lay-filter="venue-submit">
|
||||
提交
|
||||
</button>
|
||||
<a href="{:PluginsAdminUrl('vr_ticket/admin/venue/index')}" class="layui-btn layui-btn-primary">
|
||||
返回
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="__STATIC__/layui/layui.js"></script>
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||
<script>
|
||||
(function() {
|
||||
// ============================================================
|
||||
// Vue3 编辑器
|
||||
// ============================================================
|
||||
var COLOR_PRESETS = [
|
||||
'#e74c3c', '#3498db', '#2ecc71', '#9b59b6',
|
||||
'#f39c12', '#1abc9c', '#34495e', '#e67e22',
|
||||
'#16a085', '#8e44ad', '#27ae60', '#c0392b'
|
||||
];
|
||||
|
||||
var DEFAULT_ZONE = { char: '', name: '', price: 0, color: '#3498db' };
|
||||
|
||||
var app = Vue.createApp({
|
||||
data: function() {
|
||||
return {
|
||||
venue: { name: '', address: '', image: '' },
|
||||
zones: [
|
||||
{ char: 'A', name: 'VIP区', price: 899, color: '#e74c3c' },
|
||||
{ char: 'B', name: '看台区', price: 599, color: '#3498db'}}
|
||||
],
|
||||
seatMapRows: ['AAAAAA', 'BBBBBB', 'CCCCCC'],
|
||||
colorPresets: COLOR_PRESETS
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
activeZones: function() {
|
||||
var map = {};
|
||||
this.zones.forEach(function(z) {
|
||||
if (z.char.trim()) map[z.char.trim().toUpperCase()] = z;
|
||||
});
|
||||
return map;
|
||||
},
|
||||
zonesJson: function() {
|
||||
return JSON.stringify(this.zones);
|
||||
},
|
||||
seatMapRowsJson: function() {
|
||||
return JSON.stringify(this.seatMapRows);
|
||||
},
|
||||
venueJson: function() {
|
||||
return JSON.stringify(this.venue);
|
||||
},
|
||||
totalSeats: function() {
|
||||
var total = 0;
|
||||
this.seatMapRows.forEach(function(row) {
|
||||
if (row && row.trim) total += row.trim().length;
|
||||
});
|
||||
return total;
|
||||
}}
|
||||
},
|
||||
methods: {
|
||||
getZoneColor: function(ch) {
|
||||
var z = this.activeZones[ch.toUpperCase()];
|
||||
return z ? z.color : '#cccccc';
|
||||
},
|
||||
getZone: function(ch) {
|
||||
return this.activeZones[ch.toUpperCase()] || null;
|
||||
},
|
||||
getSeatTooltip: function(rowIdx, colIdx, ch) {
|
||||
var zone = this.getZone(ch);
|
||||
if (!zone) return ch + (colIdx + 1);
|
||||
return zone.name + ' · ¥' + zone.price + ' · ' + ch + (colIdx + 1);
|
||||
},
|
||||
addZone: function() {
|
||||
this.zones.push(Vue.util.extend({}, DEFAULT_ZONE));
|
||||
},
|
||||
removeZone: function(idx) {
|
||||
this.zones.splice(idx, 1);
|
||||
},
|
||||
applyPresetColor: function(color) {
|
||||
// 把颜色应用到最后一个未填写颜色的 zone
|
||||
var lastEmpty = -1;
|
||||
for (var i = 0; i < this.zones.length; i++) {
|
||||
if (!this.zones[i].color || this.zones[i].color === '#cccccc') {
|
||||
lastEmpty = i;
|
||||
}}
|
||||
}}
|
||||
if (lastEmpty >= 0) {
|
||||
this.zones[lastEmpty].color = color;
|
||||
} else {
|
||||
this.zones[this.zones.length - 1].color = color;
|
||||
}}
|
||||
},
|
||||
addSeatRow: function() {
|
||||
var lastRow = this.seatMapRows.length > 0
|
||||
? this.seatMapRows[this.seatMapRows.length - 1]
|
||||
: 'A';
|
||||
var ch = lastRow.trim().split('')[0] || 'A';
|
||||
var count = 6;
|
||||
this.seatMapRows.push(new Array(count + 1).join(ch));
|
||||
},
|
||||
removeSeatRow: function(idx) {
|
||||
this.seatMapRows.splice(idx, 1);
|
||||
},
|
||||
updateSeatMapRow: function(idx, event) {
|
||||
this.seatMapRows[idx] = event.target.value.toUpperCase();
|
||||
},
|
||||
onZoneChange: function() {
|
||||
// 强制 Vue 重新渲染预览(响应式已处理,这里可扩展自定义校验)
|
||||
}}
|
||||
},
|
||||
mounted: function() {
|
||||
// 编辑时回填
|
||||
var zonesRaw = '{$info.zones_json|raw}' || '[]';
|
||||
var mapRaw = '{$info.map_json|raw}' || '[]';
|
||||
var venueRaw = '{$info.venue_json|raw}' || '{}';
|
||||
|
||||
if (zonesRaw && zonesRaw !== '[]') {
|
||||
try { this.zones = JSON.parse(zonesRaw); } catch(e) {}
|
||||
}}
|
||||
if (mapRaw && mapRaw !== '[]') {
|
||||
try { this.seatMapRows = JSON.parse(mapRaw); } catch(e) {}
|
||||
}}
|
||||
if (venueRaw && venueRaw !== '{}') {
|
||||
try { this.venue = JSON.parse(venueRaw); } catch(e) {}
|
||||
}}
|
||||
}}
|
||||
});
|
||||
|
||||
app.mount('#venue-editor');
|
||||
|
||||
// ============================================================
|
||||
// layui 初始化
|
||||
// ============================================================
|
||||
layui.use(['form', 'layer'], function(){
|
||||
var form = layui.form;
|
||||
var layer = layui.layer;
|
||||
|
||||
form.on('submit(venue-submit)', function(data){
|
||||
// 数据已通过隐藏字段序列化,此处不做额外处理
|
||||
return true;
|
||||
});
|
||||
|
||||
form.render();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>核销记录 - VR票务</title>
|
||||
{{include file="public/head" }}
|
||||
</head>
|
||||
<body>
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">核销记录</div>
|
||||
<div class="layui-card-body">
|
||||
<div class="layui-form layui-form-pane">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">关键词</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="keywords" placeholder="票码/核销员" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">核销员</label>
|
||||
<div class="layui-input-inline">
|
||||
<select name="verifier_id">
|
||||
<option value="">全部</option>
|
||||
{foreach $verifiers as $id => $name}
|
||||
<option value="{$id}">{$name}</option>
|
||||
{/foreach}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">日期</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="start_date" id="start_date" placeholder="开始日期" class="layui-input laydate" autocomplete="off">
|
||||
</div>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="end_date" id="end_date" placeholder="结束日期" class="layui-input laydate" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button class="layui-btn" lay-submit lay-filter="search">搜索</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="layui-hide" id="table" lay-filter="table"></table>
|
||||
|
||||
<script type="text/template" id="actionTpl">
|
||||
<span style="color:#999;font-size:12px">-</span>
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{include file="public/footer" }}
|
||||
<script>
|
||||
layui.use(['table', 'laydate'], function() {
|
||||
var table = layui.table;
|
||||
var laydate = layui.laydate;
|
||||
var form = layui.form;
|
||||
|
||||
laydate.render({elem: '#start_date'});
|
||||
laydate.render({elem: '#end_date'});
|
||||
|
||||
table.render({
|
||||
elem: '#table',
|
||||
url: '{:PluginsAdminUrl("vr_ticket", "verification", "list")}',
|
||||
cols: [[
|
||||
{field: 'id', title: 'ID', width: 70},
|
||||
{field: 'ticket_code', title: '票码', width: 200},
|
||||
{field: 'goods_title', title: '商品', minWidth: 150},
|
||||
{field: 'real_name', title: '观演人', width: 100},
|
||||
{field: 'seat_info', title: '座位', minWidth: 120},
|
||||
{field: 'verifier_name', title: '核销员', width: 100},
|
||||
{field: 'created_at', title: '核销时间', width: 160, templet: function(d) {
|
||||
return d.created_at > 0 ? layui.util.toDateString(d.created_at * 1000) : '-';
|
||||
}},
|
||||
]]
|
||||
});
|
||||
|
||||
form.on('submit(search)', function(data) {
|
||||
table.reload('table', {where: data.field, page: {curr: 1}});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>核销员管理 - VR票务</title>
|
||||
{{include file="public/head" }}
|
||||
</head>
|
||||
<body>
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">核销员管理</div>
|
||||
<div class="layui-card-body">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="layui-form layui-form-pane">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">关键词</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="keywords" value="" placeholder="姓名/用户ID" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">状态</label>
|
||||
<div class="layui-input-inline">
|
||||
<select name="status" lay-search>
|
||||
<option value="">全部</option>
|
||||
<option value="1">启用</option>
|
||||
<option value="0">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button class="layui-btn" lay-submit lay-filter="search">搜索</button>
|
||||
<a href="{:MyUrl('plugins_vr_ticket/admin/verifier/save')}" class="layui-btn layui-btn-normal">添加核销员</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<table class="layui-hide" id="table" lay-filter="table"></table>
|
||||
|
||||
<script type="text/template" id="statusTpl">
|
||||
{{# if (d.status == 1) { }}
|
||||
<span class="layui-badge layui-bg-green">启用</span>
|
||||
{{# } else { }}
|
||||
<span class="layui-badge layui-bg-gray">禁用</span>
|
||||
{{# } }}
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="actionTpl">
|
||||
<a href="{:MyUrl('plugins_vr_ticket/admin/verifier/save')}?id={{d.id}}" class="layui-btn layui-btn-xs">编辑</a>
|
||||
<a href="javascript:;" class="layui-btn layui-btn-danger layui-btn-xs" lay-fn="del" data-id="{{d.id}}">禁用</a>
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{include file="public/footer" }}
|
||||
<script>
|
||||
layui.use('table', function() {
|
||||
var table = layui.table;
|
||||
var form = layui.form;
|
||||
|
||||
table.render({
|
||||
elem: '#table',
|
||||
url: '{:MyUrl("plugins_vr_ticket/admin/verifier/list")}',
|
||||
cols: [[
|
||||
{field: 'id', title: 'ID', width: 80},
|
||||
{field: 'name', title: '核销员名称', minWidth: 120},
|
||||
{field: 'user_id', title: '用户ID', width: 100},
|
||||
{field: 'user_name', title: '关联用户', minWidth: 150},
|
||||
{field: 'status', title: '状态', width: 100, templet: '#statusTpl'},
|
||||
{field: 'created_at', title: '创建时间', width: 180, templet: function(d) {
|
||||
return d.created_at > 0 ? layui.util.toDateString(d.created_at * 1000) : '-';
|
||||
}},
|
||||
{field: 'action', title: '操作', width: 150, templet: '#actionTpl'},
|
||||
]]
|
||||
});
|
||||
|
||||
form.on('submit(search)', function(data) {
|
||||
table.reload('table', {where: data.field, page: {curr: 1}});
|
||||
return false;
|
||||
});
|
||||
|
||||
$(document).on('click', '[lay-fn="del"]', function() {
|
||||
var id = $(this).data('id');
|
||||
layer.confirm('确认禁用该核销员?', function(index) {
|
||||
$.post('{:MyUrl("plugins_vr_ticket/admin/verifier/delete")}', {id: id}, function(res) {
|
||||
if (res.code == 0) {
|
||||
layer.msg('操作成功');
|
||||
table.reload('table');
|
||||
} else {
|
||||
layer.msg(res.msg || '操作失败');
|
||||
}}
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{$info ? '编辑' : '添加'}核销员 - VR票务</title>
|
||||
{{include file="public/head" }}
|
||||
</head>
|
||||
<body>
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">{$info ? '编辑' : '添加'}核销员</div>
|
||||
<div class="layui-card-body">
|
||||
<form class="layui-form" lay-filter="form">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">关联用户</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="user_id" lay-search required lay-verify="required" {if isset($info.id) && $info.id > 0}disabled{/if}>
|
||||
<option value="">请选择用户</option>
|
||||
{foreach $users as $u}
|
||||
<option value="{$u.id}" {if isset($info.user_id) && $info.user_id == $u.id}selected{/if}>
|
||||
{$u.nickname|default=$u.username|default='用户'}{$u.username ? ' / '.$u.username : ''} (ID:{$u.id})
|
||||
</option>
|
||||
{/foreach}
|
||||
</select>
|
||||
</div>
|
||||
{if isset($info.id) && $info.id > 0}
|
||||
<input type="hidden" name="user_id" value="{$info.user_id}">
|
||||
<div class="layui-form-mid layui-word-aux">用户关联后不可修改</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">核销员名称</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="name" value="{$info.name|default=''}" required lay-verify="required" placeholder="如:张三(检票员)" class="layui-input" style="width:400px">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">状态</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="checkbox" name="status" value="1" lay-skin="switch" lay-text="启用|禁用" {if !isset($info.status) || $info.status == 1}checked{/if}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<input type="hidden" name="id" value="{$info.id|default=0}">
|
||||
<button class="layui-btn" lay-submit lay-filter="submit">保存</button>
|
||||
<a href="{:MyUrl('plugins_vr_ticket/admin/verifier/list')}" class="layui-btn layui-btn-primary">返回</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{include file="public/footer" }}
|
||||
<script>
|
||||
layui.use('form', function() {
|
||||
var form = layui.form;
|
||||
form.on('submit(submit)', function(data) {
|
||||
$.post(window.location.href, data.field, function(res) {
|
||||
if (res.code == 0) {
|
||||
layer.msg(res.msg || '保存成功', function() {
|
||||
location.href = '{:MyUrl("plugins_vr_ticket/admin/verifier/list")}';
|
||||
});
|
||||
} else {
|
||||
layer.msg(res.msg || '保存失败');
|
||||
}}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
<?php
|
||||
/**
|
||||
* VR票务插件 - 独立后台路由
|
||||
*
|
||||
* URL格式: admin.php?s=vr_ticket.plugins/{action}
|
||||
* 权限: 通过 vrt_power 表的 control='VrTicket' + action 匹配
|
||||
*/
|
||||
namespace app\plugins\vr_ticket\admin\controller;
|
||||
|
||||
class Plugins extends Base
|
||||
{
|
||||
// 允许的动作(与 vrt_power action 对应)
|
||||
private $allowed_actions = [
|
||||
'seat_template_list' => 'SeatTemplateList',
|
||||
'seat_template_save' => 'SeatTemplateSave',
|
||||
'ticket_list' => 'TicketList',
|
||||
'ticket_detail' => 'TicketDetail',
|
||||
'ticket_save' => 'TicketSave',
|
||||
'ticket_verify' => 'TicketVerify',
|
||||
'ticket_export' => 'TicketExport',
|
||||
'verifier_list' => 'VerifierList',
|
||||
'verifier_save' => 'VerifierSave',
|
||||
'verification_list' => 'VerificationList',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
// 主入口: admin.php?s=vr_ticket.plugins/index
|
||||
public function Index()
|
||||
{
|
||||
$action = input('action', '', 'trim');
|
||||
if (empty($action) || !isset($this->allowed_actions[$action])) {
|
||||
return 'Invalid action: ' . htmlspecialchars($action);
|
||||
}
|
||||
$method = $this->allowed_actions[$action];
|
||||
return $this->$method();
|
||||
}
|
||||
|
||||
// 座位模板列表
|
||||
public function SeatTemplateList()
|
||||
{
|
||||
$file = dirname(__DIR__) . '/view/seat_template/list.html';
|
||||
if (file_exists($file)) {
|
||||
return view($file);
|
||||
}
|
||||
return 'Template not found: seat_template/list.html';
|
||||
}
|
||||
|
||||
// 座位模板保存
|
||||
public function SeatTemplateSave()
|
||||
{
|
||||
$file = dirname(__DIR__) . '/view/seat_template/save.html';
|
||||
if (file_exists($file)) {
|
||||
return view($file);
|
||||
}
|
||||
return 'Template not found: seat_template/save.html';
|
||||
}
|
||||
|
||||
// 电子票列表
|
||||
public function TicketList()
|
||||
{
|
||||
$file = dirname(__DIR__) . '/view/ticket/list.html';
|
||||
if (file_exists($file)) {
|
||||
return view($file);
|
||||
}
|
||||
return 'Template not found: ticket/list.html';
|
||||
}
|
||||
|
||||
// 电子票详情
|
||||
public function TicketDetail()
|
||||
{
|
||||
$file = dirname(__DIR__) . '/view/ticket/detail.html';
|
||||
if (file_exists($file)) {
|
||||
return view($file);
|
||||
}
|
||||
return 'Template not found: ticket/detail.html';
|
||||
}
|
||||
|
||||
// 电子票保存
|
||||
public function TicketSave()
|
||||
{
|
||||
$file = dirname(__DIR__) . '/view/ticket/save.html';
|
||||
if (file_exists($file)) {
|
||||
return view($file);
|
||||
}
|
||||
return 'Template not found: ticket/save.html';
|
||||
}
|
||||
|
||||
// 手动核销
|
||||
public function TicketVerify()
|
||||
{
|
||||
$file = dirname(__DIR__) . '/view/ticket/verify.html';
|
||||
if (file_exists($file)) {
|
||||
return view($file);
|
||||
}
|
||||
return 'Template not found: ticket/verify.html';
|
||||
}
|
||||
|
||||
// 导出票
|
||||
public function TicketExport()
|
||||
{
|
||||
$file = dirname(__DIR__) . '/view/ticket/export.html';
|
||||
if (file_exists($file)) {
|
||||
return view($file);
|
||||
}
|
||||
return 'Template not found: ticket/export.html';
|
||||
}
|
||||
|
||||
// 核销员列表
|
||||
public function VerifierList()
|
||||
{
|
||||
$file = dirname(__DIR__) . '/view/verifier/list.html';
|
||||
if (file_exists($file)) {
|
||||
return view($file);
|
||||
}
|
||||
return 'Template not found: verifier/list.html';
|
||||
}
|
||||
|
||||
// 核销员保存
|
||||
public function VerifierSave()
|
||||
{
|
||||
$file = dirname(__DIR__) . '/view/verifier/save.html';
|
||||
if (file_exists($file)) {
|
||||
return view($file);
|
||||
}
|
||||
return 'Template not found: verifier/save.html';
|
||||
}
|
||||
|
||||
// 核销记录
|
||||
public function VerificationList()
|
||||
{
|
||||
$file = dirname(__DIR__) . '/view/verification/list.html';
|
||||
if (file_exists($file)) {
|
||||
return view($file);
|
||||
}
|
||||
return 'Template not found: verification/list.html';
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
namespace app\plugins\vr_ticket\admin\controller;
|
||||
|
||||
class SeatTemplate extends Base
|
||||
class Seattemplate extends Base
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
/**
|
||||
* VR票务插件 - 应用初始化
|
||||
*
|
||||
* 注册插件服务命名空间的自动加载
|
||||
*/
|
||||
|
||||
spl_autoload_register(function ($class) {
|
||||
// 只处理 app\plugins\vr_ticket 命名空间
|
||||
$prefix = 'app\\plugins\\vr_ticket\\';
|
||||
$len = strlen($prefix);
|
||||
if (strncmp($prefix, $class, $len) !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 相对类名
|
||||
$relative_class = substr($class, $len);
|
||||
|
||||
// 映射到插件目录
|
||||
$file = __DIR__ . '/' . str_replace('\\', '/', $relative_class) . '.php';
|
||||
|
||||
if (file_exists($file)) {
|
||||
require $file;
|
||||
}
|
||||
}, true, true);
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
"title": "VR票务",
|
||||
"icon": "icon icon-ticket",
|
||||
"submenus": [
|
||||
{ "title": "场馆配置", "url": "/plugins/vr_ticket/admin/venue/list" },
|
||||
{ "title": "座位模板", "url": "/plugins/vr_ticket/admin/seat_template/list" },
|
||||
{ "title": "电子票", "url": "/plugins/vr_ticket/admin/ticket/list" },
|
||||
{ "title": "核销员", "url": "/plugins/vr_ticket/admin/verifier/list" },
|
||||
|
|
|
|||
Loading…
Reference in New Issue