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
parent
cd0e70e9d5
commit
683dee75dc
|
|
@ -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/
|
||||||
|
|
|
||||||
|
|
@ -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/admin(7处修正) |
|
||||||
|
| `6c18dd38f` | docs: 追加经验#19 — AdminSidebarInit items vs item 属性名陷阱 |
|
||||||
|
| `44553442e` | fix(admin): 修复菜单注册项属性名 item→items(ShopXO 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.php(input 过滤器 + 短码自动识别):**
|
||||||
|
- `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.html:UI 标题更新
|
||||||
|
- 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-ajax(verify.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(安全-P0):M-01/M-02/M-05/M-06 已修完,待关闭
|
||||||
|
- Issue #7(安全-P1):M-08 等项待审阅确认
|
||||||
|
- Issue #22:Phase 4/Phase 2 状态已更新
|
||||||
|
- EXPERIENCES.md:新增经验条目 #4(视图路径)、#19(items 属性名)
|
||||||
|
|
|
||||||
|
|
@ -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'] ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.exp(code = 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']),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 payload(HMAC-SHA256 签名,30分钟有效)
|
// Step 3: 生成 QR payload(HMAC-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(),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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')}}
|
||||||
|
|
@ -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')}}
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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')}}
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue