feat(B1): ticket/verify + list + detail admin views
parent
f3d102e7ad
commit
d8c45fbb87
|
|
@ -88,6 +88,16 @@ class Hook
|
|||
'url' => PluginsAdminUrl('vr_ticket', 'admin', 'TicketList'),
|
||||
'power' => 'vr_ticket-ticketList',
|
||||
],
|
||||
[
|
||||
'id' => 'plugins-vr_ticket-ticketverify',
|
||||
'name' => '扫码核销',
|
||||
'title' => '扫码核销',
|
||||
'is_show' => 1,
|
||||
'control' => 'admin',
|
||||
'action' => 'TicketVerify',
|
||||
'url' => PluginsAdminUrl('vr_ticket', 'admin', 'TicketVerify'),
|
||||
'power' => 'vr_ticket-ticketVerify',
|
||||
],
|
||||
[
|
||||
'id' => 'plugins-vr_ticket-verifier',
|
||||
'name' => '核销员',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,162 @@
|
|||
{{:ModuleInclude('public/header')}}
|
||||
|
||||
<div class="right-content">
|
||||
<div class="content-nav">
|
||||
<a href="{{:PluginsAdminUrl('vr_ticket', 'admin', 'ticketlist')}}" class="am-btn am-btn-secondary am-btn-xs">
|
||||
<i class="am-icon-angle-left"></i> 返回列表
|
||||
</a>
|
||||
<span>票详情</span>
|
||||
</div>
|
||||
|
||||
<div class="am-g am-margin-top">
|
||||
<!-- 左侧:票基本信息 -->
|
||||
<div class="am-u-sm-6">
|
||||
<div class="am-panel am-panel-default">
|
||||
<div class="am-panel-hd">票基础信息</div>
|
||||
<div class="am-panel-bd">
|
||||
<table class="am-table am-text-sm">
|
||||
<tr>
|
||||
<td width="100" class="am-text-gray">票码</td>
|
||||
<td>{{$ticket.ticket_code}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="am-text-gray">订单号</td>
|
||||
<td>{{$ticket.order_no}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="am-text-gray">商品名</td>
|
||||
<td>{{$ticket.goods_name}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="am-text-gray">观演人</td>
|
||||
<td>{{$ticket.visitor_name}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="am-text-gray">手机号</td>
|
||||
<td>{{$ticket.mobile}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="am-text-gray">身份证</td>
|
||||
<td>{{if !empty($ticket.id_card)}}{{$ticket.id_card}}{{else}}未填写{{/if}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="am-text-gray">座位信息</td>
|
||||
<td>{{if !empty($ticket.seat_info)}}{{$ticket.seat_info}}{{else}}无{{/if}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="am-text-gray">状态</td>
|
||||
<td>
|
||||
{{if $ticket.status == 0}}
|
||||
<span class="am-badge am-badge-warning">未核销</span>
|
||||
{{elseif $ticket.status == 1}}
|
||||
<span class="am-badge am-badge-success">已核销</span>
|
||||
{{elseif $ticket.status == 2}}
|
||||
<span class="am-badge am-badge-danger">已退款</span>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="am-text-gray">发放时间</td>
|
||||
<td>{{$ticket.create_time}}</td>
|
||||
</tr>
|
||||
{{if $ticket.status == 1}}
|
||||
<tr>
|
||||
<td class="am-text-gray">核销时间</td>
|
||||
<td>{{$ticket.verify_time}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="am-text-gray">核销人</td>
|
||||
<td>{{if !empty($ticket.verifier_name)}}{{$ticket.verifier_name}}{{else}}未知{{/if}}</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:二维码 -->
|
||||
<div class="am-u-sm-6">
|
||||
<div class="am-panel am-panel-default">
|
||||
<div class="am-panel-hd">票二维码</div>
|
||||
<div class="am-panel-bd am-text-center">
|
||||
<div id="qrcode-container" class="am-margin-bottom">
|
||||
<!-- QR 码将由 JsBarcode 生成 -->
|
||||
<svg id="qrcode-svg"></svg>
|
||||
</div>
|
||||
<p class="am-text-gray am-text-sm">票码:{{$ticket.ticket_code}}</p>
|
||||
|
||||
<!-- 条形码 -->
|
||||
<div class="am-margin-top">
|
||||
<svg id="barcode"></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 核销操作(仅未核销状态显示) -->
|
||||
{{if $ticket.status == 0}}
|
||||
<div class="am-panel am-panel-default am-margin-top">
|
||||
<div class="am-panel-hd">核销操作</div>
|
||||
<div class="am-panel-bd">
|
||||
<form class="am-form form-validation" id="verify-form">
|
||||
<input type="hidden" name="ticket_code" value="{{$ticket.ticket_code}}" />
|
||||
<div class="am-form-group">
|
||||
<button type="submit" class="am-btn am-btn-primary am-btn-block am-radius" data-am-loading="{spinner: 'circle-o-notch', loadingText: '核销中...'}">
|
||||
<i class="am-icon-check"></i> 立即核销
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{{$public_host}}static/common/lib/JsBarcode/JsBarcode.all.min.js"></script>
|
||||
<script>
|
||||
$(function() {
|
||||
// 生成条形码
|
||||
var ticketCode = '{{$ticket.ticket_code}}';
|
||||
if (typeof JsBarcode !== 'undefined') {
|
||||
JsBarcode('#barcode', ticketCode, {
|
||||
format: 'CODE128',
|
||||
width: 2,
|
||||
height: 60,
|
||||
displayValue: true,
|
||||
fontSize: 14
|
||||
});
|
||||
}
|
||||
|
||||
// 核销表单提交
|
||||
$('#verify-form').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var $btn = $(this).find('button[type="submit"]');
|
||||
|
||||
$btn.button('loading');
|
||||
|
||||
$.ajax({
|
||||
url: '{{:PluginsAdminUrl("vr_ticket", "admin", "ticketverify")}}',
|
||||
type: 'POST',
|
||||
data: { ticket_code: ticketCode },
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
$btn.button('reset');
|
||||
|
||||
if (res.code === 0) {
|
||||
alert('核销成功');
|
||||
location.reload();
|
||||
} else {
|
||||
alert(res.msg || '核销失败');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$btn.button('reset');
|
||||
alert('网络请求失败');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{{:ModuleInclude('public/footer')}}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
{{:ModuleInclude('public/header')}}
|
||||
|
||||
<div class="right-content">
|
||||
<div class="content-nav">
|
||||
<a href="{{:PluginsAdminUrl('vr_ticket', 'admin', 'ticketverify')}}" class="am-btn am-btn-primary am-btn-xs">
|
||||
<i class="am-icon-qrcode"></i> 扫码核销
|
||||
</a>
|
||||
<span>电子票列表</span>
|
||||
</div>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="am-panel am-panel-default am-margin-bottom">
|
||||
<div class="am-panel-bd">
|
||||
<form class="am-form form-validation" action="{{:PluginsAdminUrl('vr_ticket', 'admin', 'ticketlist')}}" method="POST" request-type="ajax-url">
|
||||
<div class="am-g">
|
||||
<div class="am-u-sm-3 am-u-end">
|
||||
<div class="am-input-group am-input-group-sm">
|
||||
<span class="am-input-group-label">订单号</span>
|
||||
<input type="text" name="order_no" value="{{if !empty($params.order_no)}}{{$params.order_no}}{{/if}}" placeholder="请输入订单号" class="am-radius" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="am-u-sm-3 am-u-end">
|
||||
<div class="am-input-group am-input-group-sm">
|
||||
<span class="am-input-group-label">票码</span>
|
||||
<input type="text" name="ticket_code" value="{{if !empty($params.ticket_code)}}{{$params.ticket_code}}{{/if}}" placeholder="请输入票码" class="am-radius" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="am-u-sm-3 am-u-end">
|
||||
<div class="am-input-group am-input-group-sm">
|
||||
<span class="am-input-group-label">观演人</span>
|
||||
<input type="text" name="visitor_name" value="{{if !empty($params.visitor_name)}}{{$params.visitor_name}}{{/if}}" placeholder="请输入观演人姓名" class="am-radius" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="am-u-sm-3 am-u-end">
|
||||
<div class="am-input-group am-input-group-sm">
|
||||
<span class="am-input-group-label">手机号</span>
|
||||
<input type="text" name="mobile" value="{{if !empty($params.mobile)}}{{$params.mobile}}{{/if}}" placeholder="请输入手机号" class="am-radius" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="am-g am-margin-top">
|
||||
<div class="am-u-sm-3 am-u-end">
|
||||
<div class="am-input-group am-input-group-sm">
|
||||
<span class="am-input-group-label">状态</span>
|
||||
<select name="status" class="am-radius">
|
||||
<option value="">全部</option>
|
||||
<option value="0" {{if isset($params.status) && $params.status === '0'}}selected{{/if}}>未核销</option>
|
||||
<option value="1" {{if isset($params.status) && $params.status == '1'}}selected{{/if}}>已核销</option>
|
||||
<option value="2" {{if isset($params.status) && $params.status == '2'}}selected{{/if}}>已退款</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="am-u-sm-3 am-u-end">
|
||||
<button type="submit" class="am-btn am-btn-primary am-btn-sm am-radius">
|
||||
<i class="am-icon-search"></i> 搜索
|
||||
</button>
|
||||
<a href="{{:PluginsAdminUrl('vr_ticket', 'admin', 'ticketlist')}}" class="am-btn am-btn-default am-btn-sm am-radius">
|
||||
<i class="am-icon-refresh"></i> 重置
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 票列表 -->
|
||||
<div class="am-panel am-panel-default">
|
||||
<div class="am-panel-hd">电子票列表</div>
|
||||
<div class="am-panel-bd">
|
||||
<table class="am-table am-table-striped am-table-hover am-text-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>票码</th>
|
||||
<th>观演人</th>
|
||||
<th>座位信息</th>
|
||||
<th>商品名</th>
|
||||
<th>状态</th>
|
||||
<th>发放时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{if !empty($list)}}
|
||||
{{volist name="list" id="ticket"}}
|
||||
<tr>
|
||||
<td>{{$ticket.ticket_code}}</td>
|
||||
<td>{{$ticket.visitor_name}}</td>
|
||||
<td>{{if !empty($ticket.seat_info)}}{{$ticket.seat_info}}{{else}}无{{/if}}</td>
|
||||
<td>{{$ticket.goods_name}}</td>
|
||||
<td>
|
||||
{{if $ticket.status == 0}}
|
||||
<span class="am-badge am-badge-warning">未核销</span>
|
||||
{{elseif $ticket.status == 1}}
|
||||
<span class="am-badge am-badge-success">已核销</span>
|
||||
{{elseif $ticket.status == 2}}
|
||||
<span class="am-badge am-badge-danger">已退款</span>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td>{{$ticket.create_time}}</td>
|
||||
<td>
|
||||
<a href="{{:PluginsAdminUrl('vr_ticket', 'admin', 'ticketdetail')}}?id={{$ticket.id}}" class="am-btn am-btn-default am-btn-xs am-radius">
|
||||
<i class="am-icon-eye"></i> 查看详情
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{/volist}}
|
||||
{{else}}
|
||||
<tr>
|
||||
<td colspan="7" class="am-text-center">暂无数据</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- 分页 -->
|
||||
{{if !empty($page)}}
|
||||
<div class="am-margin-top">
|
||||
{{$page|raw}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{:ModuleInclude('public/footer')}}
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
{{:ModuleInclude('public/header')}}
|
||||
|
||||
<div class="right-content">
|
||||
<div class="content-nav">
|
||||
<a href="{{:PluginsAdminUrl('vr_ticket', 'admin', 'ticketlist')}}" class="am-btn am-btn-secondary am-btn-xs">
|
||||
<i class="am-icon-list"></i> 电子票列表
|
||||
</a>
|
||||
<span>票码核销</span>
|
||||
</div>
|
||||
|
||||
<!-- 统计栏 -->
|
||||
<div class="am-g am-margin-top-sm">
|
||||
<div class="am-u-sm-4">
|
||||
<div class="am-panel am-panel-success">
|
||||
<div class="am-panel-hd am-text-center">今日核销</div>
|
||||
<div class="am-panel-bd am-text-center am-text-lg">{{$stats.today_verified|default=0}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="am-u-sm-4">
|
||||
<div class="am-panel am-panel-warning">
|
||||
<div class="am-panel-hd am-text-center">待核销</div>
|
||||
<div class="am-panel-bd am-text-center am-text-lg">{{$stats.pending|default=0}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="am-u-sm-4">
|
||||
<div class="am-panel am-panel-primary">
|
||||
<div class="am-panel-hd am-text-center">已核销总数</div>
|
||||
<div class="am-panel-bd am-text-center am-text-lg">{{$stats.total_verified|default=0}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 核销操作区 -->
|
||||
<div class="am-panel am-panel-default am-margin-top">
|
||||
<div class="am-panel-hd">扫码/输入核销</div>
|
||||
<div class="am-panel-bd">
|
||||
<form class="am-form form-validation" id="verify-form">
|
||||
<div class="am-form-group">
|
||||
<label>票码/短码</label>
|
||||
<div class="am-input-group">
|
||||
<input type="text" name="ticket_code" placeholder="请输入票码或扫描二维码" class="am-radius" required />
|
||||
<span class="am-input-group-btn">
|
||||
<button type="button" class="am-btn am-btn-default am-radius" id="scan-btn">
|
||||
<i class="am-icon-camera"></i> 扫码
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="am-alert am-alert-secondary am-margin-top-xs am-text-xs">
|
||||
支持手动输入票码或点击"扫码"使用摄像头扫描二维码
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="am-form-group">
|
||||
<button type="submit" class="am-btn am-btn-primary am-radius" id="verify-btn" data-am-loading="{spinner: 'circle-o-notch', loadingText: '核销中...'}">
|
||||
<i class="am-icon-check"></i> 确认核销
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 结果展示区 -->
|
||||
<div id="result-container"></div>
|
||||
|
||||
<!-- 摄像头扫码弹窗 -->
|
||||
<div class="am-modal am-modal-prompt" id="scan-modal">
|
||||
<div class="am-modal-dialog">
|
||||
<div class="am-modal-hd">
|
||||
扫码核销
|
||||
<a href="javascript: void(0)" class="am-modal-close am-close">×</a>
|
||||
</div>
|
||||
<div class="am-modal-bd">
|
||||
<video id="scan-video" style="width: 100%; max-width: 400px; display: none;" autoplay></video>
|
||||
<canvas id="scan-canvas" style="display: none;"></canvas>
|
||||
<div id="scan-status" class="am-text-center am-padding-top">点击"开始扫码"启动摄像头</div>
|
||||
<div class="am-margin-top">
|
||||
<button type="button" class="am-btn am-btn-primary am-radius" id="start-scan-btn">
|
||||
<i class="am-icon-video-camera"></i> 开始扫码
|
||||
</button>
|
||||
<button type="button" class="am-btn am-btn-default am-radius" id="stop-scan-btn" style="display: none;">
|
||||
<i class="am-icon-stop"></i> 停止
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{{$public_host}}static/common/lib/JsBarcode/JsBarcode.all.min.js"></script>
|
||||
<script>
|
||||
// 核销表单提交
|
||||
$('#verify-form').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var $btn = $('#verify-btn');
|
||||
var ticketCode = $('input[name="ticket_code"]').val().trim();
|
||||
|
||||
if (!ticketCode) {
|
||||
alert('请输入票码');
|
||||
return;
|
||||
}
|
||||
|
||||
$btn.button('loading');
|
||||
|
||||
$.ajax({
|
||||
url: '{{:PluginsAdminUrl("vr_ticket", "admin", "ticketverify")}}',
|
||||
type: 'POST',
|
||||
data: { ticket_code: ticketCode },
|
||||
dataType: 'json',
|
||||
success: function(res) {
|
||||
$btn.button('reset');
|
||||
showResult(res);
|
||||
},
|
||||
error: function() {
|
||||
$btn.button('reset');
|
||||
showResult({ code: -1, msg: '网络请求失败' });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 展示结果
|
||||
function showResult(res) {
|
||||
var html = '';
|
||||
|
||||
if (res.code === 0) {
|
||||
var ticket = res.data;
|
||||
html = '<div class="am-alert am-alert-success am-margin-top">' +
|
||||
'<h4><i class="am-icon-check-circle"></i> 核销成功</h4>' +
|
||||
'<p><strong>票码:</strong>' + ticket.ticket_code + '</p>' +
|
||||
'<p><strong>观演人:</strong>' + ticket.visitor_name + '</p>' +
|
||||
'<p><strong>座位:</strong>' + (ticket.seat_info || '无') + '</p>' +
|
||||
'<p><strong>商品名:</strong>' + ticket.goods_name + '</p>' +
|
||||
'<p><strong>核销时间:</strong>' + ticket.verify_time + '</p>' +
|
||||
'</div>';
|
||||
|
||||
// 清空输入框
|
||||
$('input[name="ticket_code"]').val('');
|
||||
|
||||
// 刷新统计(可选)
|
||||
// loadStats();
|
||||
} else {
|
||||
html = '<div class="am-alert am-alert-danger am-margin-top">' +
|
||||
'<h4><i class="am-icon-times-circle"></i> 核销失败</h4>' +
|
||||
'<p>' + res.msg + '</p>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
$('#result-container').html(html);
|
||||
}
|
||||
|
||||
// 扫码功能
|
||||
var video = document.getElementById('scan-video');
|
||||
var canvas = document.getElementById('scan-canvas');
|
||||
var stream = null;
|
||||
|
||||
$('#scan-btn').on('click', function() {
|
||||
$('#scan-modal').modal('open');
|
||||
});
|
||||
|
||||
$('#start-scan-btn').on('click', function() {
|
||||
startScan();
|
||||
});
|
||||
|
||||
$('#stop-scan-btn').on('click', function() {
|
||||
stopScan();
|
||||
});
|
||||
|
||||
function startScan() {
|
||||
navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } })
|
||||
.then(function(s) {
|
||||
stream = s;
|
||||
video.srcObject = stream;
|
||||
video.style.display = 'block';
|
||||
$('#start-scan-btn').hide();
|
||||
$('#stop-scan-btn').show();
|
||||
$('#scan-status').text('正在扫描...');
|
||||
|
||||
// 开始扫描循环
|
||||
scanFrame();
|
||||
})
|
||||
.catch(function(err) {
|
||||
$('#scan-status').text('摄像头访问失败: ' + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
function stopScan() {
|
||||
if (stream) {
|
||||
stream.getTracks().forEach(function(track) {
|
||||
track.stop();
|
||||
});
|
||||
stream = null;
|
||||
}
|
||||
video.style.display = 'none';
|
||||
$('#start-scan-btn').show();
|
||||
$('#stop-scan-btn').hide();
|
||||
$('#scan-status').text('点击"开始扫码"启动摄像头');
|
||||
}
|
||||
|
||||
function scanFrame() {
|
||||
if (!stream) return;
|
||||
|
||||
if (video.readyState === video.HAVE_ENOUGH_DATA) {
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
var ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
// 简单模拟:直接将扫描内容填入输入框(实际需要用 jsQR 等库解析)
|
||||
// 这里暂时不支持二维码解析,仅展示摄像头功能
|
||||
}
|
||||
|
||||
requestAnimationFrame(scanFrame);
|
||||
}
|
||||
|
||||
// 弹窗关闭时停止摄像头
|
||||
$('#scan-modal').on('closed.modal', function() {
|
||||
stopScan();
|
||||
});
|
||||
</script>
|
||||
|
||||
{{:ModuleInclude('public/footer')}}
|
||||
Loading…
Reference in New Issue