fix(B): Admin.php 短码识别 + BaseService QR签名加强(含code字段)

- Admin.php: input过滤器 null→'' + TicketVerify自动识别短码(<20字符无连字符)
- BaseService: signQrPayload/verifyQrPayload 签名内容增加 ticket_code 字段
- TicketService: refreshQrPayload 签名验证增加 code 字段校验
- Admin 视图: 5个文件 UI 统一(form-validation/submit-ajax/统计栏初始值)
- venue/list.html: 移除多余工具栏按钮
- .gitignore: 增加 .agent/.claude/.gitnexus/graphify-out/
- DEVELPOMENT_LOG.md: 追加 2026-04-25~27 B端核销冲刺记录
feat/b-verification-clean
Council 2026-05-11 00:21:25 +08:00
parent cd0e70e9d5
commit 683dee75dc
12 changed files with 501 additions and 370 deletions

6
.gitignore vendored
View File

@ -16,3 +16,9 @@ shopxo/public/adminufgeyw.php
# 强制追踪 vr_ticket 插件的 database 目录(全局 gitignore 的 database 规则过宽) # 强制追踪 vr_ticket 插件的 database 目录(全局 gitignore 的 database 规则过宽)
!shopxo/app/plugins/vr_ticket/database/ !shopxo/app/plugins/vr_ticket/database/
#插件目录
.agent/
.claude/
.gitnexus/
graphify-out/

View File

@ -778,3 +778,88 @@ if (empty($room['id'])) {
- ✅ antigravity 测试通过(基本没问题) - ✅ antigravity 测试通过(基本没问题)
- ✅ 西莉雅代码审查通过(读取优先级 + 防御层双重保障) - ✅ 西莉雅代码审查通过(读取优先级 + 防御层双重保障)
- ✅ 多模板模式修复验证 - ✅ 多模板模式修复验证
---
## 2026-04-25 至 2026-04-27 B端核销冲刺周末完成
### 分支状态
- 分支:`feat/b-verification-clean`(从 `feat/b-verification` 合并而来)
- HEAD`cd0e70e9d remove: 座位模板页面和控制器(被场馆配置覆盖)`
### 本次提交记录a873aac14 → cd0e70e9d
| Commit | 说明 |
|--------|------|
| `cd0e70e9d` | remove: 座位模板页面和控制器(被场馆配置覆盖) |
| `a5b2d00c8` | fix(admin): visitor_name→real_name, verify_status, TicketVerify GET handler, input filter null→s |
| `6fae40698` | docs: 强化经验#4 — 插件视图路径 view/{group}/ 结构图解 |
| `51bcc207f` | fix(admin): MyView路径 admin/view→view/admin7处修正 |
| `6c18dd38f` | docs: 追加经验#19 — AdminSidebarInit items vs item 属性名陷阱 |
| `44553442e` | fix(admin): 修复菜单注册项属性名 item→itemsShopXO sidebar 渲染器期望 items 复数) |
| `23d2b2f7b` | fix(B): TicketVerify M-05修复verifier_id从session + 新增TicketStats API |
| `c93cc1134` | feat(B): B端核销功能合入session key + $this->admin 修正) |
| `a873aac14` | docs: Phase B plan + README status update |
### 未提交变更Working Directory
> 用户要求只写文档不修改代码,以下变更在本地工作区,尚未 commit。
**Admin.phpinput 过滤器 + 短码自动识别):**
- `input('xxx', '', null, 'trim')``input('xxx', '', '', 'trim')` 修复 null→空字符串问题
- TicketVerify 新增 `verifyByShortCode()` 分支逻辑:长度 < 20 且不含连字符视为短码
**视图 UI 统一升级5个文件**
- 所有 admin 页面统一改为 `.page-title` 大标题风格icon + 标题 + 底部分隔线)
- ticket/list.html搜索表单改为 POST + `form-validation` + 搜索按钮 loading 状态 + 重置按钮
- ticket/verify.html**核心重写** — 统计栏初始值 `-`(不再显示「加载中...」)+ form-validation submit-ajax 模式 + ShopXO 原生 submit-ajax 回调
- verification/list.html搜索表单 POST 化 + 表格包裹 panel + 统计条数
- verifier/list.html搜索表单 POST 化 + 启用/禁用改用 ShopXO `submit-ajax`(不再用 onclick 手动 ajax
- setup.htmlUI 标题更新
- venue/list.html移除顶部「插件全局设置」工具栏按钮
### Phase B 完成度
| 子项 | 状态 | 说明 |
|------|------|------|
| B0-1 Admin.php 权限校验 | ✅ 完成 | session('admin_id') 校验 |
| B0-2 verifier_id session来源 | ✅ 完成 | M-05 已修复 |
| B0-3 ALTER TABLE bug | ✅ 完成 | B端合入时已修 |
| B1-1 扫码核销页面 | ✅ 可用 | verify.html 基本可用,大量细节待修 |
| B1-2 电子票列表 | ✅ 可用 | list.html 已可用 |
| B1-3 票详情 | ✅ 可用 | detail.html 已生成 |
| B2-1 核销员管理 | ✅ 可用 | verifier/list.html + save.html |
| B2-2 核销记录 | ✅ 可用 | verification/list.html |
| B2-3 座位模板管理 | ✅ 已移除 | 被场馆配置覆盖,无需独立页面 |
### 关键修复记录
**TicketVerify 短码自动识别Admin.php**
```php
$is_short_code = (strlen($ticket_code) < 20 && strpos($ticket_code, '-') === false);
if ($is_short_code) {
$result = TicketService::verifyByShortCode($ticket_code, $verifier_id);
} else {
$result = TicketService::verifyTicket($ticket_code, $verifier_id);
}
```
**ShopXO form-validation submit-ajaxverify.html**
- 原:手动 `$.ajax()` + `$('#verify-form').on('submit', e.preventDefault)`
- 新:`<form class="form-validation" request-type="ajax-url">` + `$(document).on('submit-success.form-validation')`
- ShopXO 自动处理loading 状态 / 错误提示 / 成功后回调
### 待修复细节(已知问题)
| 问题 | 优先级 | 说明 |
|------|--------|------|
| verify.html 扫码摄像头 | 🟡 中 | 摄像头调用代码已写但无二维码解析库jsQR扫码后无法自动填入输入框 |
| verification/list.html 日期筛选 | 🟡 中 | datepicker 可能未初始化 |
| verifier/save.html | 🟡 中 | 添加核销员表单未确认 |
| ticket/detail.html QR码显示 | 🟡 中 | detail 页面已生成QR码渲染待验证 |
### 文档同步
- Issue #6(安全-P0M-01/M-02/M-05/M-06 已修完,待关闭
- Issue #7(安全-P1M-08 等项待审阅确认
- Issue #22Phase 4/Phase 2 状态已更新
- EXPERIENCES.md新增经验条目 #4(视图路径)、#19items 属性名)

View File

@ -123,23 +123,23 @@ class Admin extends Common
$where = []; $where = [];
// 逐字段精确搜索(对应视图的独立搜索框) // 逐字段精确搜索(对应视图的独立搜索框)
$order_no = input('order_no', '', null, 'trim'); $order_no = input('order_no', '', '', 'trim');
if ($order_no !== '') { if ($order_no !== '') {
$where[] = ['order_no', 'like', "%{$order_no}%"]; $where[] = ['order_no', 'like', "%{$order_no}%"];
} }
$ticket_code = input('ticket_code', '', 's', 'trim'); $ticket_code = input('ticket_code', '', '', 'trim');
if ($ticket_code !== '') { if ($ticket_code !== '') {
$where[] = ['ticket_code', 'like', "%{$ticket_code}%"]; $where[] = ['ticket_code', 'like', "%{$ticket_code}%"];
} }
$real_name = input('real_name', '', null, 'trim'); $real_name = input('real_name', '', '', 'trim');
if ($real_name !== '') { if ($real_name !== '') {
$where[] = ['real_name', 'like', "%{$real_name}%"]; $where[] = ['real_name', 'like', "%{$real_name}%"];
} }
$phone = input('phone', '', null, 'trim'); $phone = input('phone', '', '', 'trim');
if ($phone !== '') { if ($phone !== '') {
$where[] = ['phone', 'like', "%{$phone}%"]; $where[] = ['phone', 'like', "%{$phone}%"];
} }
$verify_status = input('verify_status', '', null); $verify_status = input('verify_status', '', '', 'trim');
if ($verify_status !== '' && $verify_status !== null) { if ($verify_status !== '' && $verify_status !== null) {
$where[] = ['verify_status', '=', intval($verify_status)]; $where[] = ['verify_status', '=', intval($verify_status)];
} }
@ -237,7 +237,7 @@ class Admin extends Common
return json(['code' => -1, 'msg' => '非法请求']); return json(['code' => -1, 'msg' => '非法请求']);
} }
$ticket_code = input('ticket_code', '', 's', 'trim'); $ticket_code = input('ticket_code', '', '', 'trim');
// M-05: verifier_id 从 session 获取,禁止客户端伪造 // M-05: verifier_id 从 session 获取,禁止客户端伪造
// $this->admin 来自父类构造函数AdminService::LoginInfo() // $this->admin 来自父类构造函数AdminService::LoginInfo()
@ -255,7 +255,17 @@ class Admin extends Common
return DataReturn('票码不能为空', -1); return DataReturn('票码不能为空', -1);
} }
$result = \app\plugins\vr_ticket\service\TicketService::verifyTicket($ticket_code, $verifier_id); // 自动识别短码 vs UUID短码长度 < 20 且不含连字符
$is_short_code = (strlen($ticket_code) < 20 && strpos($ticket_code, '-') === false);
if ($is_short_code) {
// 短码核销
$result = \app\plugins\vr_ticket\service\TicketService::verifyByShortCode($ticket_code, $verifier_id);
} else {
// UUID 核销
$result = \app\plugins\vr_ticket\service\TicketService::verifyTicket($ticket_code, $verifier_id);
}
return DataReturn($result['msg'], $result['code'], $result['data'] ?? []); return DataReturn($result['msg'], $result['code'], $result['data'] ?? []);
} }

View File

@ -489,8 +489,8 @@ class BaseService
public static function signQrPayload(array $payload): string public static function signQrPayload(array $payload): string
{ {
$secret = self::getVrSecret(); $secret = self::getVrSecret();
// 签名内容:id.g.iat.exp // 签名内容:code.id.g.iat.expcode = ticket_code
$sign_str = $payload['id'] . '.' . $payload['g'] . '.' . $payload['iat'] . '.' . $payload['exp']; $sign_str = $payload['code'] . '.' . $payload['id'] . '.' . $payload['g'] . '.' . $payload['iat'] . '.' . $payload['exp'];
$sig = substr(hash_hmac('sha256', $sign_str, $secret), 0, 8); $sig = substr(hash_hmac('sha256', $sign_str, $secret), 0, 8);
$payload['sig'] = $sig; $payload['sig'] = $sig;
@ -515,8 +515,8 @@ class BaseService
return null; return null;
} }
// 必填字段检查 // 必填字段检查(包含 ticket_code
if (!isset($payload['id'], $payload['g'], $payload['iat'], $payload['exp'], $payload['sig'])) { if (!isset($payload['id'], $payload['g'], $payload['code'], $payload['iat'], $payload['exp'], $payload['sig'])) {
return null; return null;
} }
@ -525,9 +525,9 @@ class BaseService
return null; return null;
} }
// 签名验证 // 签名验证(包含 ticket_code
$secret = self::getVrSecret(); $secret = self::getVrSecret();
$sign_str = $payload['id'] . '.' . $payload['g'] . '.' . $payload['iat'] . '.' . $payload['exp']; $sign_str = $payload['code'] . '.' . $payload['id'] . '.' . $payload['g'] . '.' . $payload['iat'] . '.' . $payload['exp'];
$expected_sig = substr(hash_hmac('sha256', $sign_str, $secret), 0, 8); $expected_sig = substr(hash_hmac('sha256', $sign_str, $secret), 0, 8);
if (!hash_equals($expected_sig, $payload['sig'])) { if (!hash_equals($expected_sig, $payload['sig'])) {
@ -535,9 +535,10 @@ class BaseService
} }
return [ return [
'id' => intval($payload['id']), 'id' => intval($payload['id']),
'g' => intval($payload['g']), 'g' => intval($payload['g']),
'exp' => intval($payload['exp']), 'code' => $payload['code'],
'exp' => intval($payload['exp']),
]; ];
} }
} }

View File

@ -187,10 +187,11 @@ class TicketService extends BaseService
// 短码存储在 qr_data 中,供前端展示 // 短码存储在 qr_data 中,供前端展示
$short_code = BaseService::shortCodeEncode($og['goods_id'], $ticket_id); $short_code = BaseService::shortCodeEncode($og['goods_id'], $ticket_id);
// Step 3: 生成 QR payloadHMAC-SHA256 签名30分钟有效 // Step 3: 生成 QR payloadHMAC-SHA256 签名30分钟有效,含 ticket_code
$qr_payload = BaseService::signQrPayload([ $qr_payload = BaseService::signQrPayload([
'id' => $ticket_id, 'id' => $ticket_id,
'g' => $og['goods_id'], 'g' => $og['goods_id'],
'code' => $ticket_code,
'iat' => $now, 'iat' => $now,
'exp' => $now + 1800, // 30分钟 'exp' => $now + 1800, // 30分钟
]); ]);
@ -516,16 +517,22 @@ class TicketService extends BaseService
$decoded = BaseService::verifyQrPayload($payload); $decoded = BaseService::verifyQrPayload($payload);
if ($decoded !== null && $decoded['exp'] - time() > 900) { if ($decoded !== null && $decoded['exp'] - time() > 900) {
// 有效期 > 15分钟返回缓存 // 有效期 > 15分钟返回缓存
return [ // 签名验证(包含 ticket_code
'code' => 0, $secret = self::getVrSecret();
'msg' => 'success', $sign_str = $decoded['code'] . '.' . $decoded['id'] . '.' . $decoded['g'] . '.' . $decoded['iat'] . '.' . $decoded['exp'];
'data' => [ $expected_sig = substr(hash_hmac('sha256', $sign_str, $secret), 0, 8);
'short_code' => $short_code, if (isset($decoded['sig']) && $decoded['sig'] === $expected_sig) {
'payload' => $payload, return [
'cached' => true, 'code' => 0,
'expires_in' => $decoded['exp'] - time(), 'msg' => 'success',
], 'data' => [
]; 'short_code' => $short_code,
'payload' => $payload,
'cached' => true,
'expires_in' => $decoded['exp'] - time(),
],
];
}
} }
} }

View File

@ -196,10 +196,11 @@ class WalletService extends BaseService
$expiresAt = $now + self::QR_TTL; $expiresAt = $now + self::QR_TTL;
$payload = [ $payload = [
'id' => $ticket['id'], 'id' => $ticket['id'],
'g' => $ticket['goods_id'], 'g' => $ticket['goods_id'],
'iat' => $now, 'code' => $ticket['ticket_code'],
'exp' => $expiresAt, 'iat' => $now,
'exp' => $expiresAt,
]; ];
$encoded = self::signQrPayload($payload); $encoded = self::signQrPayload($payload);

View File

@ -1,11 +1,19 @@
{{:ModuleInclude('public/header')}} {{:ModuleInclude('public/header')}}
<style>
.page-title {
font-size: 18px;
font-weight: 600;
color: #1a1a2e;
padding: 24px 0 16px 0;
border-bottom: 2px solid #e8e8e8;
margin-bottom: 20px;
}
</style>
<div class="right-content"> <div class="right-content">
<div class="content-nav"> <div class="page-title">
<a href="{{:PluginsAdminUrl('vr_ticket', 'admin', 'index')}}" class="am-btn am-btn-secondary am-btn-xs"> <i class="am-icon-cog am-margin-right-sm"></i>插件设置
<i class="am-icon-angle-left"></i> 返回
</a>
<span>插件设置</span>
</div> </div>
<div class="content-body"> <div class="content-body">

View File

@ -1,40 +1,56 @@
{{:ModuleInclude('public/header')}} {{:ModuleInclude('public/header')}}
<style>
.page-title {
font-size: 18px;
font-weight: 600;
color: #1a1a2e;
padding: 24px 0 16px 0;
border-bottom: 2px solid #e8e8e8;
margin-bottom: 20px;
}
</style>
<div class="right-content"> <div class="right-content">
<div class="content-nav"> <div class="page-title">
<a href="{{:PluginsAdminUrl('vr_ticket', 'admin', 'ticketverify')}}" class="am-btn am-btn-primary am-btn-xs"> <i class="am-icon-ticket am-margin-right-sm"></i>电子票列表
<i class="am-icon-qrcode"></i> 扫码核销
</a>
<span>电子票列表</span>
</div> </div>
<!-- 搜索栏 --> <!-- 搜索栏 -->
<div class="am-panel am-panel-default am-margin-bottom"> <div class="am-panel am-panel-default am-radius am-margin-bottom">
<div class="am-panel-bd"> <div class="am-panel-bd">
<form class="am-form" action="{{:PluginsAdminUrl('vr_ticket', 'admin', 'ticketlist')}}" method="GET"> <form class="am-form form-validation form-search" request-type="form"
action="{{:PluginsAdminUrl('vr_ticket', 'admin', 'ticketlist')}}" method="POST">
<div class="am-g"> <div class="am-g">
<div class="am-u-sm-3 am-u-end"> <div class="am-u-sm-3 am-u-end">
<div class="am-input-group am-input-group-sm"> <div class="am-input-group am-input-group-sm">
<span class="am-input-group-label">订单号</span> <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" /> <input type="text" name="order_no"
value="{{if !empty($params.order_no)}}{{$params.order_no}}{{/if}}" placeholder="请输入订单号"
class="am-radius" />
</div> </div>
</div> </div>
<div class="am-u-sm-3 am-u-end"> <div class="am-u-sm-3 am-u-end">
<div class="am-input-group am-input-group-sm"> <div class="am-input-group am-input-group-sm">
<span class="am-input-group-label">票码</span> <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" /> <input type="text" name="ticket_code"
value="{{if !empty($params.ticket_code)}}{{$params.ticket_code}}{{/if}}"
placeholder="请输入票码" class="am-radius" />
</div> </div>
</div> </div>
<div class="am-u-sm-3 am-u-end"> <div class="am-u-sm-3 am-u-end">
<div class="am-input-group am-input-group-sm"> <div class="am-input-group am-input-group-sm">
<span class="am-input-group-label">观演人</span> <span class="am-input-group-label">观演人</span>
<input type="text" name="real_name" value="{{if !empty($params.real_name)}}{{$params.real_name}}{{/if}}" placeholder="请输入观演人姓名" class="am-radius" /> <input type="text" name="real_name"
value="{{if !empty($params.real_name)}}{{$params.real_name}}{{/if}}"
placeholder="请输入观演人姓名" class="am-radius" />
</div> </div>
</div> </div>
<div class="am-u-sm-3 am-u-end"> <div class="am-u-sm-3 am-u-end">
<div class="am-input-group am-input-group-sm"> <div class="am-input-group am-input-group-sm">
<span class="am-input-group-label">手机号</span> <span class="am-input-group-label">手机号</span>
<input type="text" name="phone" value="{{if !empty($params.phone)}}{{$params.phone}}{{/if}}" placeholder="请输入手机号" class="am-radius" /> <input type="text" name="phone" value="{{if !empty($params.phone)}}{{$params.phone}}{{/if}}"
placeholder="请输入手机号" class="am-radius" />
</div> </div>
</div> </div>
</div> </div>
@ -45,17 +61,22 @@
<span class="am-input-group-label">状态</span> <span class="am-input-group-label">状态</span>
<select name="verify_status" class="am-radius"> <select name="verify_status" class="am-radius">
<option value="">全部</option> <option value="">全部</option>
<option value="0" {{if isset($params.verify_status) && $params.verify_status === '0'}}selected{{/if}}>未核销</option> <option value="0" {{if isset($params.verify_status) && $params.verify_status==='0'
<option value="1" {{if isset($params.verify_status) && $params.verify_status == '1'}}selected{{/if}}>已核销</option> }}selected{{/if}}>未核销</option>
<option value="2" {{if isset($params.verify_status) && $params.verify_status == '2'}}selected{{/if}}>已退款</option> <option value="1" {{if isset($params.verify_status) && $params.verify_status=='1'
}}selected{{/if}}>已核销</option>
<option value="2" {{if isset($params.verify_status) && $params.verify_status=='2'
}}selected{{/if}}>已退款</option>
</select> </select>
</div> </div>
</div> </div>
<div class="am-u-sm-3 am-u-end"> <div class="am-u-sm-3 am-u-end">
<button type="submit" class="am-btn am-btn-primary am-btn-sm am-radius"> <button type="submit" class="am-btn am-btn-primary am-btn-sm am-radius"
data-am-loading="{spinner:'circle-o-notch', loadingText:'搜索中...'}">
<i class="am-icon-search"></i> 搜索 <i class="am-icon-search"></i> 搜索
</button> </button>
<a href="{{:PluginsAdminUrl('vr_ticket', 'admin', 'ticketlist')}}" class="am-btn am-btn-default am-btn-sm am-radius"> <a href="{{:PluginsAdminUrl('vr_ticket', 'admin', 'ticketlist')}}"
class="am-btn am-btn-default am-btn-sm am-radius am-margin-left-xs">
<i class="am-icon-refresh"></i> 重置 <i class="am-icon-refresh"></i> 重置
</a> </a>
</div> </div>
@ -82,33 +103,35 @@
</thead> </thead>
<tbody> <tbody>
{{if !empty($list)}} {{if !empty($list)}}
{{volist name="list" id="ticket"}} {{volist name="list" id="ticket"}}
<tr> <tr>
<td>{{$ticket.ticket_code}}</td> <td>{{$ticket.ticket_code}}</td>
<td>{{$ticket.real_name|default='—'}}</td> <td>{{$ticket.real_name|default='—'}}</td>
<td>{{if !empty($ticket.seat_info)}}{{$ticket.seat_info}}{{else}}无{{/if}}</td> <td>{{if !empty($ticket.seat_info)}}{{$ticket.seat_info}}{{else}}无{{/if}}</td>
<td>{{$ticket.goods_title|default='已删除商品'}}</td> <td>{{$ticket.goods_title|default='已删除商品'}}</td>
<td> <td>
{{if $ticket.verify_status == 0}} {{if $ticket.verify_status == 0}}
<span class="am-badge am-badge-warning">未核销</span> <span class="am-badge am-badge-warning">未核销</span>
{{elseif $ticket.verify_status == 1}} {{elseif $ticket.verify_status == 1}}
<span class="am-badge am-badge-success">已核销</span> <span class="am-badge am-badge-success">已核销</span>
{{elseif $ticket.verify_status == 2}} {{elseif $ticket.verify_status == 2}}
<span class="am-badge am-badge-danger">已退款</span> <span class="am-badge am-badge-danger">已退款</span>
{{/if}} {{/if}}
</td> </td>
<td>{{if !empty($ticket.issued_at)}}{{:date('Y-m-d H:i', $ticket.issued_at)}}{{else}}—{{/if}}</td> <td>{{if !empty($ticket.issued_at)}}{{:date('Y-m-d H:i', $ticket.issued_at)}}{{else}}—{{/if}}
<td> </td>
<a href="{{:PluginsAdminUrl('vr_ticket', 'admin', 'ticketdetail')}}?id={{$ticket.id}}" class="am-btn am-btn-default am-btn-xs am-radius"> <td>
<i class="am-icon-eye"></i> 查看详情 <a href="{{:PluginsAdminUrl('vr_ticket', 'admin', 'ticketdetail')}}?id={{$ticket.id}}"
</a> class="am-btn am-btn-default am-btn-xs am-radius">
</td> <i class="am-icon-eye"></i> 查看详情
</tr> </a>
{{/volist}} </td>
</tr>
{{/volist}}
{{else}} {{else}}
<tr> <tr>
<td colspan="7" class="am-text-center">暂无数据</td> <td colspan="7" class="am-text-center">暂无数据</td>
</tr> </tr>
{{/if}} {{/if}}
</tbody> </tbody>
</table> </table>
@ -123,4 +146,4 @@
</div> </div>
</div> </div>
{{:ModuleInclude('public/footer')}} {{:ModuleInclude('public/footer')}}

View File

@ -1,40 +1,50 @@
{{:ModuleInclude('public/header')}} {{:ModuleInclude('public/header')}}
<style>
.page-title {
font-size: 18px;
font-weight: 600;
color: #1a1a2e;
padding: 24px 0 16px 0;
border-bottom: 2px solid #e8e8e8;
margin-bottom: 20px;
}
</style>
<div class="right-content"> <div class="right-content">
<div class="content-nav"> <div class="page-title">
<a href="{{:PluginsAdminUrl('vr_ticket', 'admin', 'ticketlist')}}" class="am-btn am-btn-secondary am-btn-xs"> <i class="am-icon-qrcode am-margin-right-sm"></i>票码核销
<i class="am-icon-list"></i> 电子票列表
</a>
<span>票码核销</span>
</div> </div>
<!-- 统计栏 --> <!-- 统计栏 -->
<div class="am-g am-margin-top-sm"> <div class="am-g am-margin-top-sm">
<div class="am-u-sm-4"> <div class="am-u-sm-4">
<div class="am-panel am-panel-success"> <div class="am-panel am-panel-success am-radius">
<div class="am-panel-hd am-text-center">今日核销</div> <div class="am-panel-hd am-text-center">今日核销</div>
<div class="am-panel-bd am-text-center am-text-lg" id="stat-today">加载中...</div> <div class="am-panel-bd am-text-center am-text-lg" id="stat-today">-</div>
</div> </div>
</div> </div>
<div class="am-u-sm-4"> <div class="am-u-sm-4">
<div class="am-panel am-panel-warning"> <div class="am-panel am-panel-warning am-radius">
<div class="am-panel-hd am-text-center">待核销</div> <div class="am-panel-hd am-text-center">待核销</div>
<div class="am-panel-bd am-text-center am-text-lg" id="stat-pending">加载中...</div> <div class="am-panel-bd am-text-center am-text-lg" id="stat-pending">-</div>
</div> </div>
</div> </div>
<div class="am-u-sm-4"> <div class="am-u-sm-4">
<div class="am-panel am-panel-primary"> <div class="am-panel am-panel-primary am-radius">
<div class="am-panel-hd am-text-center">已核销总数</div> <div class="am-panel-hd am-text-center">已核销总数</div>
<div class="am-panel-bd am-text-center am-text-lg" id="stat-verified">加载中...</div> <div class="am-panel-bd am-text-center am-text-lg" id="stat-verified">-</div>
</div> </div>
</div> </div>
</div> </div>
<!-- 核销操作区 --> <!-- 核销操作区 -->
<div class="am-panel am-panel-default am-margin-top"> <div class="am-panel am-panel-default am-radius am-margin-top">
<div class="am-panel-hd">扫码/输入核销</div> <div class="am-panel-hd">扫码/输入核销</div>
<div class="am-panel-bd"> <div class="am-panel-bd">
<form class="am-form" id="verify-form"> <form class="am-form form-validation" id="verify-form"
action="{{:PluginsAdminUrl('vr_ticket', 'admin', 'TicketVerify')}}" method="POST"
request-type="ajax-url" request-value="{{:PluginsAdminUrl('vr_ticket', 'admin', 'TicketVerify')}}">
<div class="am-form-group"> <div class="am-form-group">
<label>票码/短码</label> <label>票码/短码</label>
<div class="am-input-group"> <div class="am-input-group">
@ -51,7 +61,7 @@
</div> </div>
<div class="am-form-group"> <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: '核销中...'}"> <button type="submit" class="am-btn am-btn-primary am-radius">
<i class="am-icon-check"></i> 确认核销 <i class="am-icon-check"></i> 确认核销
</button> </button>
</div> </div>
@ -77,7 +87,8 @@
<button type="button" class="am-btn am-btn-primary am-radius" id="start-scan-btn"> <button type="button" class="am-btn am-btn-primary am-radius" id="start-scan-btn">
<i class="am-icon-video-camera"></i> 开始扫码 <i class="am-icon-video-camera"></i> 开始扫码
</button> </button>
<button type="button" class="am-btn am-btn-default am-radius" id="stop-scan-btn" style="display: none;"> <button type="button" class="am-btn am-btn-default am-radius" id="stop-scan-btn"
style="display: none;">
<i class="am-icon-stop"></i> 停止 <i class="am-icon-stop"></i> 停止
</button> </button>
</div> </div>
@ -88,161 +99,123 @@
<script src="{{$public_host}}static/common/lib/JsBarcode/JsBarcode.all.min.js"></script> <script src="{{$public_host}}static/common/lib/JsBarcode/JsBarcode.all.min.js"></script>
<script> <script>
// 核销表单提交 // 页面加载时获取统计
$('#verify-form').on('submit', function(e) { $(function () {
e.preventDefault(); loadStats();
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) { function loadStats() {
var html = ''; $.ajax({
url: '{{:PluginsAdminUrl('vr_ticket', 'admin', 'TicketStats')}}',
if (res.code === 0) { type: 'POST',
var ticket = res.data; dataType: 'json',
html = '<div class="am-alert am-alert-success am-margin-top">' + success: function (res) {
'<h4><i class="am-icon-check-circle"></i> 核销成功</h4>' + if (res.code === 0 && res.data) {
'<p><strong>观演人:</strong>' + (ticket.real_name || '—') + '</p>' + $('#stat-today').text(res.data.today || 0);
'<p><strong>座位:</strong>' + (ticket.seat_info || '无') + '</p>' + $('#stat-pending').text(res.data.pending || 0);
'<p><strong>商品名:</strong>' + (ticket.goods_name || '—') + '</p>' + $('#stat-verified').text(res.data.verified || 0);
'</div>'; }
// 清空输入框
$('input[name="ticket_code"]').val('');
// 刷新统计(可选)
// loadStats();
function loadStats() {
$.ajax({
url: '{{:PluginsAdminUrl('vr_ticket', 'admin', 'TicketStats')}}',
type: 'POST',
dataType: 'json',
success: function(res) {
if (res.code === 0 && res.data) {
$('#stat-today').text(res.data.today || 0);
$('#stat-pending').text(res.data.pending || 0);
$('#stat-verified').text(res.data.verified || 0);
} else {
$('#stat-today').text('-');
$('#stat-pending').text('-');
$('#stat-verified').text('-');
} }
}, });
error: function() { }
$('#stat-today').text('-');
$('#stat-pending').text('-'); // ShopXO submit-ajax 成功回调:展示核销结果
$('#stat-verified').text('-'); $(document).on('submit-success.form-validation', '#verify-form', function (e, res) {
var html = '';
if (res.code === 0) {
var ticket = res.data || {};
html = '<div class="am-alert am-alert-success am-margin-top am-radius">' +
'<h4><i class="am-icon-check-circle"></i> 核销成功</h4>' +
'<p><strong>观演人:</strong>' + (ticket.real_name || '—') + '</p>' +
'<p><strong>座位:</strong>' + (ticket.seat_info || '无') + '</p>' +
'<p><strong>商品名:</strong>' + (ticket.goods_name || '—') + '</p>' +
'</div>';
// 清空输入框
$('input[name="ticket_code"]').val('');
// 刷新统计
loadStats();
} else {
html = '<div class="am-alert am-alert-danger am-margin-top am-radius">' +
'<h4><i class="am-icon-times-circle"></i> 核销失败</h4>' +
'<p>' + res.msg + '</p>' +
'</div>';
} }
$('#result-container').html(html);
}); });
}
// 页面加载时获取统计 // 扫码功能
$(function() { var video = document.getElementById('scan-video');
loadStats(); var canvas = document.getElementById('scan-canvas');
}); var stream = null;
} else {
html = '<div class="am-alert am-alert-danger am-margin-top">' + $('#scan-btn').on('click', function () {
'<h4><i class="am-icon-times-circle"></i> 核销失败</h4>' + $('#scan-modal').modal('open');
'<p>' + res.msg + '</p>' + });
'</div>';
$('#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);
});
} }
$('#result-container').html(html);
}
// 扫码功能 function stopScan() {
var video = document.getElementById('scan-video'); if (stream) {
var canvas = document.getElementById('scan-canvas'); stream.getTracks().forEach(function (track) {
var stream = null; track.stop();
});
$('#scan-btn').on('click', function() { stream = null;
$('#scan-modal').modal('open'); }
}); video.style.display = 'none';
$('#start-scan-btn').show();
$('#start-scan-btn').on('click', function() { $('#stop-scan-btn').hide();
startScan(); $('#scan-status').text('点击"开始扫码"启动摄像头');
});
$('#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() { function scanFrame() {
if (!stream) return; if (!stream) return;
if (video.readyState === video.HAVE_ENOUGH_DATA) { if (video.readyState === video.HAVE_ENOUGH_DATA) {
canvas.width = video.videoWidth; canvas.width = video.videoWidth;
canvas.height = video.videoHeight; canvas.height = video.videoHeight;
var ctx = canvas.getContext('2d'); var ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0, canvas.width, canvas.height); ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
// 简单模拟:直接将扫描内容填入输入框(实际需要用 jsQR 等库解析) // 简单模拟:直接将扫描内容填入输入框(实际需要用 jsQR 等库解析)
// 这里暂时不支持二维码解析,仅展示摄像头功能 // 这里暂时不支持二维码解析,仅展示摄像头功能
}
requestAnimationFrame(scanFrame);
} }
requestAnimationFrame(scanFrame);
}
// 弹窗关闭时停止摄像头 // 弹窗关闭时停止摄像头
$('#scan-modal').on('closed.modal', function() { $('#scan-modal').on('closed.modal', function () {
stopScan(); stopScan();
}); });
</script> </script>
{{:ModuleInclude('public/footer')}} {{:ModuleInclude('public/footer')}}

View File

@ -1,21 +1,29 @@
{{:ModuleInclude('public/header')}} {{:ModuleInclude('public/header')}}
<style>
.page-title {
font-size: 18px;
font-weight: 600;
color: #1a1a2e;
padding: 24px 0 16px 0;
border-bottom: 2px solid #e8e8e8;
margin-bottom: 20px;
}
</style>
<div class="right-content"> <div class="right-content">
<div class="content-nav"> <div class="page-title">
<a href="{{:PluginsAdminUrl('vr_ticket', 'admin', 'index')}}" class="am-btn am-btn-secondary am-btn-xs"> <i class="am-icon-history am-margin-right-sm"></i>核销记录
<i class="am-icon-angle-left"></i> 返回
</a>
<span>核销记录</span>
</div> </div>
<!-- 搜索栏 --> <!-- 搜索栏 -->
<div class="am-scrollable-horizontal"> <div class="am-panel am-panel-default am-radius am-margin-bottom-sm">
<div class="am-margin-bottom-sm"> <div class="am-panel-bd am-cf">
<form action="{{:PluginsAdminUrl('vr_ticket', 'admin', 'VerificationList')}}" method="GET" class="am-form-inline"> <form action="{{:PluginsAdminUrl('vr_ticket', 'admin', 'VerificationList')}}" method="POST" class="am-form form-validation form-search am-fl" request-type="form">
<div class="am-input-group am-input-group-sm am-fl" style="width: 200px;"> <div class="am-input-group am-input-group-sm am-fl" style="width: 200px;">
<input type="text" name="keywords" value="{{$keywords|default=''}}" placeholder="搜索票码/核销员" class="am-radius" /> <input type="text" name="keywords" value="{{$keywords|default=''}}" placeholder="搜索票码/核销员" class="am-radius" />
</div> </div>
<div class="am-input-group am-input-group-sm am-fl am-margin-left-sm" style="width: 150px;"> <div class="am-input-group am-input-group-sm am-fl am-margin-left-xs" style="width: 150px;">
<select name="verifier_id"> <select name="verifier_id">
<option value="">全部核销员</option> <option value="">全部核销员</option>
{{if !empty($verifiers)}} {{if !empty($verifiers)}}
@ -25,54 +33,66 @@
{{/if}} {{/if}}
</select> </select>
</div> </div>
<div class="am-input-group am-input-group-sm am-fl am-margin-left-sm" style="width: 140px;"> <div class="am-input-group am-input-group-sm am-fl am-margin-left-xs" style="width: 140px;">
<input type="text" name="start_date" value="{{$start_date|default=''}}" placeholder="开始日期" class="am-radius" autocomplete="off" data-date-format="yyyy-mm-dd" /> <input type="text" name="start_date" value="{{$start_date|default=''}}" placeholder="开始日期" class="am-radius" autocomplete="off" data-date-format="yyyy-mm-dd" />
</div> </div>
<div class="am-input-group am-input-group-sm am-fl" style="width: 140px;"> <div class="am-input-group am-input-group-sm am-fl" style="width: 140px;">
<input type="text" name="end_date" value="{{$end_date|default=''}}" placeholder="结束日期" class="am-radius" autocomplete="off" data-date-format="yyyy-mm-dd" /> <input type="text" name="end_date" value="{{$end_date|default=''}}" placeholder="结束日期" class="am-radius" autocomplete="off" data-date-format="yyyy-mm-dd" />
</div> </div>
<button type="submit" class="am-btn am-btn-default am-btn-xs am-radius am-fl am-margin-left-sm"> <button type="submit" class="am-btn am-btn-primary am-btn-xs am-radius am-fl am-margin-left-xs" data-am-loading="{spinner:'circle-o-notch', loadingText:'搜索中...'}">
<i class="am-icon-search"></i> 搜索 <i class="am-icon-search"></i> 搜索
</button> </button>
<a href="{{:PluginsAdminUrl('vr_ticket', 'admin', 'VerificationList')}}" class="am-btn am-btn-default am-btn-xs am-radius am-fl am-margin-left-xs">
<i class="am-icon-refresh"></i> 重置
</a>
</form> </form>
</div> </div>
</div>
<!-- 表格 --> <!-- 表格 -->
<table class="am-table am-table-striped am-table-hover am-text-middle"> <div class="am-panel am-panel-default am-radius">
<thead> <div class="am-panel-hd">核销记录 <span class="am-text-xs am-padding-left">共 {{$count|default=0}} 条</span></div>
<tr> <div class="am-panel-bd">
<th>ID</th> <table class="am-table am-table-striped am-table-hover am-text-middle am-scrollable-horizontal">
<th>票码</th> <thead>
<th>核销员</th> <tr>
<th>商品</th> <th class="am-text-center" width="60">ID</th>
<th>座位信息</th> <th class="am-text-left">票码</th>
<th>观演人</th> <th class="am-text-left">核销员</th>
<th>核销时间</th> <th class="am-text-left">商品</th>
</tr> <th class="am-text-left">座位信息</th>
</thead> <th class="am-text-left">观演人</th>
<tbody> <th class="am-text-left">核销时间</th>
{{if !empty($list)}} </tr>
{{foreach $list as $item}} </thead>
<tr> <tbody>
<td>{{$item.id}}</td> {{if !empty($list)}}
<td>{{$item.ticket_code}}</td> {{foreach $list as $item}}
<td>{{$item.verifier_name|default='-'}}</td> <tr>
<td>{{$item.goods_title|default='-'}}</td> <td class="am-text-center">{{$item.id}}</td>
<td>{{$item.seat_info|default='-'}}</td> <td>{{$item.ticket_code}}</td>
<td>{{$item.real_name|default='-'}}</td> <td>{{$item.verifier_name|default='-'}}</td>
<td>{{if !empty($item.created_at)}}{{date('Y-m-d H:i', $item.created_at)}}{{/if}}</td> <td>{{$item.goods_title|default='-'}}</td>
</tr> <td>{{$item.seat_info|default='-'}}</td>
{{/foreach}} <td>{{$item.real_name|default='-'}}</td>
{{else}} <td>{{if !empty($item.created_at)}}{{$item.created_at|date='Y-m-d H:i'}}{{else}}-{{/if}}</td>
<tr> </tr>
<td colspan="7" class="am-text-center">暂无数据</td> {{/foreach}}
</tr> {{else}}
{{/if}} <tr>
</tbody> <td colspan="7" class="am-text-center">暂无数据</td>
</table> </tr>
{{/if}}
</tbody>
</table>
<!-- 分页 --> <!-- 分页 -->
{{if !empty($page)}}{{$page|raw}}{{/if}} {{if !empty($page)}}
<div class="am-margin-top-sm">
{{$page|raw}}
</div>
{{/if}}
</div>
</div> </div>
</div> </div>

View File

@ -1,104 +1,110 @@
{{:ModuleInclude('public/header')}} {{:ModuleInclude('public/header')}}
<style>
.page-title {
font-size: 18px;
font-weight: 600;
color: #1a1a2e;
padding: 24px 0 16px 0;
border-bottom: 2px solid #e8e8e8;
margin-bottom: 20px;
}
</style>
<div class="right-content"> <div class="right-content">
<div class="content-nav"> <div class="page-title">
<a href="{{:PluginsAdminUrl('vr_ticket', 'admin', 'index')}}" class="am-btn am-btn-secondary am-btn-xs"> <i class="am-icon-user am-margin-right-sm"></i>核销员管理
<i class="am-icon-angle-left"></i> 返回
</a>
<span>核销员管理</span>
</div> </div>
<!-- 搜索栏 --> <!-- 搜索栏 -->
<div class="am-scrollable-horizontal"> <div class="am-panel am-panel-default am-radius am-margin-bottom-sm">
<div class="am-margin-bottom-sm"> <div class="am-panel-bd am-cf">
<form action="{{:PluginsAdminUrl('vr_ticket', 'admin', 'VerifierList')}}" method="GET" class="am-form-inline"> <form action="{{:PluginsAdminUrl('vr_ticket', 'admin', 'VerifierList')}}" method="POST" class="am-form form-validation form-search am-fl" request-type="form">
<div class="am-input-group am-input-group-sm am-fl" style="width: 200px;"> <div class="am-input-group am-input-group-sm am-fl" style="width: 200px;">
<input type="text" name="keywords" value="{{$keywords|default=''}}" placeholder="搜索名称/用户ID" class="am-radius" /> <input type="text" name="keywords" value="{{$keywords|default=''}}" placeholder="搜索名称/用户ID" class="am-radius" />
</div> </div>
<div class="am-input-group am-input-group-sm am-fl am-margin-left-sm" style="width: 120px;"> <div class="am-input-group am-input-group-sm am-fl am-margin-left-xs" style="width: 120px;">
<select name="status" class="am-radius"> <select name="status" class="am-radius">
<option value="">全部状态</option> <option value="">全部状态</option>
<option value="1" {{if isset($status) && $status==1}}selected{{/if}}>启用</option> <option value="1" {{if isset($status) && $status==1}}selected{{/if}}>启用</option>
<option value="0" {{if isset($status) && $status==0}}selected{{/if}}>禁用</option> <option value="0" {{if isset($status) && $status==0}}selected{{/if}}>禁用</option>
</select> </select>
</div> </div>
<button type="submit" class="am-btn am-btn-default am-btn-xs am-radius am-fl am-margin-left-sm"> <button type="submit" class="am-btn am-btn-primary am-btn-xs am-radius am-fl am-margin-left-xs" data-am-loading="{spinner:'circle-o-notch', loadingText:'搜索中...'}">
<i class="am-icon-search"></i> 搜索 <i class="am-icon-search"></i> 搜索
</button> </button>
<a href="{{:PluginsAdminUrl('vr_ticket', 'admin', 'VerifierSave')}}" class="am-btn am-btn-primary am-btn-xs am-radius am-fr"> <a href="{{:PluginsAdminUrl('vr_ticket', 'admin', 'VerifierList')}}" class="am-btn am-btn-default am-btn-xs am-radius am-fl am-margin-left-xs">
<i class="am-icon-plus"></i> 添加核销员 <i class="am-icon-refresh"></i> 重置
</a> </a>
</form> </form>
<a href="{{:PluginsAdminUrl('vr_ticket', 'admin', 'VerifierSave')}}" class="am-btn am-btn-success am-btn-xs am-radius am-fr">
<i class="am-icon-plus"></i> 添加核销员
</a>
</div> </div>
</div>
<!-- 表格 --> <!-- 表格 -->
<table class="am-table am-table-striped am-table-hover am-text-middle"> <div class="am-panel am-panel-default am-radius">
<thead> <div class="am-panel-hd">核销员列表 <span class="am-text-xs am-padding-left">共 {{$count|default=0}} 条</span></div>
<tr> <div class="am-panel-bd am-scrollable-horizontal">
<th>ID</th> <table class="am-table am-table-striped am-table-hover am-text-middle am-margin-bottom-0">
<th>核销员名称</th> <thead>
<th>关联用户</th> <tr>
<th>状态</th> <th class="am-text-center" width="60">ID</th>
<th>添加时间</th> <th class="am-text-left">核销员名称</th>
<th>操作</th> <th class="am-text-left">关联用户</th>
</tr> <th class="am-text-center" width="80">状态</th>
</thead> <th class="am-text-left">添加时间</th>
<tbody> <th class="am-text-right" width="160">操作</th>
{{if !empty($list)}} </tr>
{{foreach $list as $item}} </thead>
<tr> <tbody>
<td>{{$item.id}}</td> {{if !empty($list)}}
<td>{{$item.name}}</td> {{foreach $list as $item}}
<td>{{$item.user_name|default='-'}}</td> <tr id="data-list-{{$item.id}}">
<td> <td class="am-text-center">{{$item.id}}</td>
{{if $item.status == 1}} <td>{{$item.name}}</td>
<span class="am-badge am-badge-success">启用</span> <td>{{$item.user_name|default='-'}}</td>
{{else}} <td class="am-text-center">
<span class="am-badge am-badge-danger">禁用</span> {{if $item.status == 1}}
{{/if}} <span class="am-badge am-badge-success am-radius">启用</span>
</td> {{else}}
<td>{{if !empty($item.created_at)}}{{date('Y-m-d H:i', $item.created_at)}}{{/if}}</td> <span class="am-badge am-badge-danger am-radius">禁用</span>
<td> {{/if}}
<a href="{{:PluginsAdminUrl('vr_ticket', 'admin', 'VerifierSave', ['id'=>$item.id])}}" class="am-btn am-btn-default am-btn-xs am-radius"> </td>
<i class="am-icon-edit"></i> 编辑 <td>{{if !empty($item.created_at)}}{{$item.created_at|date='Y-m-d H:i'}}{{else}}-{{/if}}</td>
</a> <td class="am-text-right view-operation">
<button type="button" class="am-btn am-btn-danger am-btn-xs am-radius" onclick="toggleVerifier({{$item.id}}, {{$item.status}})"> <a href="{{:PluginsAdminUrl('vr_ticket', 'admin', 'VerifierSave', ['id'=>$item.id])}}" class="am-btn am-btn-secondary am-btn-xs am-radius">
{{if $item.status == 1}}<i class="am-icon-ban"></i> 禁用{{else}}<i class="am-icon-check"></i> 启用{{/if}} <i class="am-icon-edit"></i> 编辑
</button> </a>
</td> {{if $item.status == 1}}
</tr> <button type="button" class="am-btn am-btn-warning am-btn-xs am-radius am-margin-left-xs submit-ajax" data-url="{{:PluginsAdminUrl('vr_ticket', 'admin', 'VerifierDelete')}}" data-id="{{$item.id}}" data-view="reload" data-msg="确定要禁用该核销员?">
{{/foreach}} <i class="am-icon-ban"></i> 禁用
{{else}} </button>
<tr> {{else}}
<td colspan="6" class="am-text-center">暂无数据</td> <button type="button" class="am-btn am-btn-success am-btn-xs am-radius am-margin-left-xs submit-ajax" data-url="{{:PluginsAdminUrl('vr_ticket', 'admin', 'VerifierDelete')}}" data-id="{{$item.id}}" data-value="enable" data-view="reload" data-msg="确定要启用该核销员?">
</tr> <i class="am-icon-check"></i> 启用
{{/if}} </button>
</tbody> {{/if}}
</table> </td>
</tr>
{{/foreach}}
{{else}}
<tr>
<td colspan="6" class="am-text-center">暂无数据</td>
</tr>
{{/if}}
</tbody>
</table>
<!-- 分页 --> <!-- 分页 -->
{{if !empty($page)}}{{$page|raw}}{{/if}} {{if !empty($page)}}
<div class="am-margin-top-sm">
{{$page|raw}}
</div>
{{/if}}
</div>
</div> </div>
</div> </div>
<script>
function toggleVerifier(id, currentStatus) {
var action = currentStatus == 1 ? '禁用' : '启用';
if (!confirm('确定' + action + '该核销员?')) return;
$.ajax({
url: '{{:PluginsAdminUrl("vr_ticket", "admin", "VerifierDelete")}}',
type: 'POST',
data: {id: id},
dataType: 'json',
success: function(res) {
if (res.code >= 0) {
location.reload();
} else {
alert(res.msg);
}
}
});
}
</script>
{{:ModuleInclude('public/footer')}} {{:ModuleInclude('public/footer')}}

View File

@ -3,15 +3,6 @@
<!-- right content start --> <!-- right content start -->
<div class="content-right" style="padding: 0 40px;"> <div class="content-right" style="padding: 0 40px;">
<div class="content"> <div class="content">
<!-- 页面顶部导航栏/工具栏 -->
<div class="am-cf am-padding-bottom-sm">
<div class="am-fr">
<a href="{{:PluginsAdminUrl('vr_ticket', 'admin', 'Setup')}}" class="am-btn am-btn-warning am-btn-sm am-radius" title="全局设置高德 API 等参数">
<i class="am-icon-cog"></i> 插件全局设置
</a>
</div>
</div>
<!-- 搜索与筛选区域 --> <!-- 搜索与筛选区域 -->
<div class="am-panel am-panel-default am-radius"> <div class="am-panel am-panel-default am-radius">
<div class="am-panel-bd"> <div class="am-panel-bd">