feat(B1): ticket/verify + list + detail admin views

feat/phase-b-verification
Council 2026-04-25 10:35:34 +08:00
parent f3d102e7ad
commit d8c45fbb87
4 changed files with 519 additions and 0 deletions

View File

@ -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' => '核销员',

View File

@ -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')}}

View File

@ -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')}}

View File

@ -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">&times;</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')}}