council(execute): FrontendDev - merge Round 3 view files to main

FrontendDev Round 3 deliverables:
- All 7 admin view files (new + URL fixes)
- Resolve plan.md conflict: keep merged version + add Round 3 summary

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
refactor/vr-ticket-20260416
Council 2026-04-15 14:11:36 +08:00
commit 8ca3b2d67b
8 changed files with 343 additions and 9 deletions

30
plan.md
View File

@ -410,6 +410,36 @@ foreach ($users_raw as $u) {
---
## Round 3 执行总结FrontendDev
### 已完成工作
| 文件 | 操作 | 说明 |
|------|------|------|
| `seat_template/list.html` | URL 修正 | `MyUrl()``PluginsAdminUrl()` |
| `seat_template/save.html` | URL 修正 | 返回按钮 URL 修正 |
| `verification/list.html` | URL 修正 | `MyUrl()``PluginsAdminUrl()` |
| `ticket/list.html` | URL 修正 | list + detail + export URL 全部修正 |
| `ticket/detail.html` | 新建 | 完整票详情页QR码/关联商品/核销信息/状态标签) |
| `verifier/list.html` | 新建 | 核销员列表页(搜索/状态筛选/禁用按钮) |
| `verifier/save.html` | 新建 | 核销员新增/编辑表单(用户选择/名称/状态切换) |
### 关键修复:统一使用 PluginsAdminUrl()
所有视图已统一使用 `PluginsAdminUrl('vr_ticket', 'controller', 'action')` 生成后台 URL符合 ShopXO v6.8.0 插件标准路由规范。
### 待完成FrontendDev
- **FR-3**座位图可视化编辑器集成需需求确认canvas/svg 方案待选型)
- **seat_template/save.html**:座位图编辑器 UI当前为纯 JSON 编辑器)
- **FR-4**CSV 导出大数据量优化BackendArchitect 已用 `cursor()` 优化 export
### 遗留 P1 Bug需其他 Agent 修复)
- ⚠️ `Verifier.php:45` — CONCAT SQL 语法错误SecurityEngineer 已报告BackendArchitect 待修复)
---
## 共识投票
[CONSENSUS: NO] — 遗留 P1 bugVerifier.php:45 CONCAT 语法错误需要修复Task S4审计日志设计尚未完成。

View File

@ -31,7 +31,7 @@
</div>
<div class="layui-inline">
<button class="layui-btn" lay-submit lay-filter="search">搜索</button>
<a href="{:MyUrl('plugins_vr_ticket/admin/seat_template/save')}" class="layui-btn layui-btn-normal">添加模板</a>
<a href="{:PluginsAdminUrl('vr_ticket', 'seat_template', 'save')}" class="layui-btn layui-btn-normal">添加模板</a>
</div>
</div>
</div>
@ -48,7 +48,7 @@
</script>
<script type="text/template" id="actionTpl">
<a href="{:MyUrl('plugins_vr_ticket/admin/seat_template/save')}?id={{d.id}}" class="layui-btn layui-btn-xs">编辑</a>
<a href="{:PluginsAdminUrl('vr_ticket', 'seat_template', 'save')}?id={{d.id}}" class="layui-btn layui-btn-xs">编辑</a>
<a href="javascript:;" class="layui-btn layui-btn-danger layui-btn-xs" lay-fn="del" data-id="{{d.id}}">删除</a>
</script>
</div>
@ -63,7 +63,7 @@ layui.use('table', function() {
table.render({
elem: '#table',
url: '{:MyUrl("plugins_vr_ticket/admin/seat_template/list")}',
url: '{:PluginsAdminUrl("vr_ticket", "seat_template", "list")}',
cols: [[
{field: 'id', title: 'ID', width: 80},
{field: 'name', title: '模板名称', minWidth: 150},
@ -85,7 +85,7 @@ layui.use('table', function() {
$(document).on('click', '[lay-fn="del"]', function() {
var id = $(this).data('id');
layer.confirm('确认删除?', function(index) {
$.post('{:MyUrl("plugins_vr_ticket/admin/seat_template/delete")}', {id: id}, function(res) {
$.post('{:PluginsAdminUrl("vr_ticket", "seat_template", "delete")}', {id: id}, function(res) {
if (res.code == 0) {
layer.msg('删除成功');
table.reload('table');

View File

@ -53,7 +53,7 @@
<div class="layui-input-block">
<input type="hidden" name="id" value="{$info.id|default=0}">
<button class="layui-btn" lay-submit lay-filter="submit">保存</button>
<a href="{:MyUrl('plugins_vr_ticket/admin/seat_template/list')}" class="layui-btn layui-btn-primary">返回</a>
<a href="{:PluginsAdminUrl('vr_ticket', 'seat_template', 'list')}" class="layui-btn layui-btn-primary">返回</a>
</div>
</div>
</form>

View File

@ -0,0 +1,123 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>票详情 - VR票务</title>
{include file="public/head" /}
<style>
.ticket-detail-card { max-width: 800px; }
.qr-code-wrap { text-align: center; margin: 10px 0; }
.qr-code-wrap img { border: 1px solid #eee; padding: 5px; }
.detail-row { display: flex; border-bottom: 1px solid #f0f0f0; padding: 8px 0; }
.detail-label { font-weight: bold; color: #666; width: 120px; flex-shrink: 0; }
.detail-value { color: #333; flex: 1; }
.status-badge { display: inline-block; padding: 2px 10px; border-radius: 3px; font-size: 12px; }
.status-0 { background: #e6f0ff; color: #1677ff; }
.status-1 { background: #f2ffe6; color: #52c41a; }
.status-2 { background: #fff1f0; color: #ff4d4f; }
</style>
</head>
<body>
<div class="layui-fluid">
<div class="layui-card ticket-detail-card">
<div class="layui-card-header">票详情</div>
<div class="layui-card-body">
{if !empty($msg)}
<blockquote class="layui-elem-quote layui-quote-nm" style="color:#ff4d4f;">{$msg}</blockquote>
<div style="margin-top:15px;">
<a href="{:PluginsAdminUrl('vr_ticket', 'ticket', 'list')}" class="layui-btn layui-btn-primary">返回列表</a>
</div>
{else}
<div class="layui-row layui-col-space20">
<!-- 左列:票基本信息 -->
<div class="layui-col-md6">
<div class="layui-form">
<div class="layui-form-item" style="margin-bottom:0;">
<label class="layui-form-label" style="width:120px;">票码</label>
<div class="layui-input-block" style="margin-left:130px;line-height:38px;font-family:monospace;font-size:14px;font-weight:bold;">{$ticket.ticket_code}</div>
</div>
<div class="layui-form-item" style="margin-bottom:0;">
<label class="layui-form-label" style="width:120px;">状态</label>
<div class="layui-input-block" style="margin-left:130px;line-height:38px;">
<span class="status-badge status-{$ticket.verify_status}">
{switch name="$ticket.verify_status"}
{case value="0"}未核销{/case}
{case value="1"}已核销{/case}
{case value="2"}已退款{/case}
{default /}未知
{/switch}
</span>
</div>
</div>
<div class="layui-form-item" style="margin-bottom:0;">
<label class="layui-form-label" style="width:120px;">观演人</label>
<div class="layui-input-block" style="margin-left:130px;line-height:38px;">{$ticket.real_name|default='-'}</div>
</div>
<div class="layui-form-item" style="margin-bottom:0;">
<label class="layui-form-label" style="width:120px;">手机号</label>
<div class="layui-input-block" style="margin-left:130px;line-height:38px;">{$ticket.phone|default='-'}</div>
</div>
<div class="layui-form-item" style="margin-bottom:0;">
<label class="layui-form-label" style="width:120px;">座位</label>
<div class="layui-input-block" style="margin-left:130px;line-height:38px;">{$ticket.seat_info|default='-'}</div>
</div>
<div class="layui-form-item" style="margin-bottom:0;">
<label class="layui-form-label" style="width:120px;">订单号</label>
<div class="layui-input-block" style="margin-left:130px;line-height:38px;">{$ticket.order_no|default='-'}</div>
</div>
<div class="layui-form-item" style="margin-bottom:0;">
<label class="layui-form-label" style="width:120px;">发放时间</label>
<div class="layui-input-block" style="margin-left:130px;line-height:38px;">
{$ticket.issued_at > 0 ? date('Y-m-d H:i:s', $ticket.issued_at) : '-'}
</div>
</div>
</div>
</div>
<!-- 右列QR码 + 关联信息 -->
<div class="layui-col-md6">
<div class="qr-code-wrap">
{if !empty($ticket.qr_code_url)}
<img src="{$ticket.qr_code_url}" alt="票二维码" width="160" height="160">
{else}
<div style="width:160px;height:160px;background:#f5f5f5;line-height:160px;text-align:center;color:#999;margin:0 auto;">暂无二维码</div>
{/if}
</div>
<div class="layui-form" style="margin-top:10px;">
<div class="layui-form-item" style="margin-bottom:0;">
<label class="layui-form-label" style="width:120px;">关联商品</label>
<div class="layui-input-block" style="margin-left:130px;line-height:38px;">
{$goods.title|default='<span style="color:#999">已删除商品</span>'|raw}
</div>
</div>
<div class="layui-form-item" style="margin-bottom:0;">
<label class="layui-form-label" style="width:120px;">核销员</label>
<div class="layui-input-block" style="margin-left:130px;line-height:38px;">
{if !empty($verifier)}
{$verifier.name} (ID:{$verifier.id})
{else /}
-
{/if}
</div>
</div>
<div class="layui-form-item" style="margin-bottom:0;">
<label class="layui-form-label" style="width:120px;">核销时间</label>
<div class="layui-input-block" style="margin-left:130px;line-height:38px;">
{$ticket.verify_time > 0 ? date('Y-m-d H:i:s', $ticket.verify_time) : '-'}
</div>
</div>
</div>
</div>
</div>
<div style="margin-top:20px;border-top:1px solid #eee;padding-top:15px;">
<a href="{:PluginsAdminUrl('vr_ticket', 'ticket', 'list')}" class="layui-btn layui-btn-primary">返回列表</a>
</div>
{/if}
</div>
</div>
</div>
{include file="public/footer" /}
</body>
</html>

View File

@ -32,7 +32,7 @@
</div>
<div class="layui-inline">
<button class="layui-btn" lay-submit lay-filter="search">搜索</button>
<a href="{:MyUrl('plugins_vr_ticket/admin/ticket/export')}" class="layui-btn layui-btn-primary" target="_blank">导出CSV</a>
<a href="{:PluginsAdminUrl('vr_ticket', 'ticket', 'export')}" class="layui-btn layui-btn-primary" target="_blank">导出CSV</a>
</div>
</div>
</div>
@ -53,7 +53,7 @@
</script>
<script type="text/template" id="actionTpl">
<a href="{:MyUrl('plugins_vr_ticket/admin/ticket/detail')}?id={{d.id}}" class="layui-btn layui-btn-xs">详情</a>
<a href="{:PluginsAdminUrl('vr_ticket', 'ticket', 'detail')}?id={{d.id}}" class="layui-btn layui-btn-xs">详情</a>
</script>
</div>
</div>
@ -67,7 +67,7 @@ layui.use(['table', 'form'], function() {
table.render({
elem: '#table',
url: '{:MyUrl("plugins_vr_ticket/admin/ticket/list")}',
url: '{:PluginsAdminUrl("vr_ticket", "ticket", "list")}',
cols: [[
{field: 'id', title: 'ID', width: 70},
{field: 'ticket_code', title: '票码', width: 200},

View File

@ -65,7 +65,7 @@ layui.use(['table', 'laydate'], function() {
table.render({
elem: '#table',
url: '{:MyUrl("plugins_vr_ticket/admin/verification/list")}',
url: '{:PluginsAdminUrl("vr_ticket", "verification", "list")}',
cols: [[
{field: 'id', title: 'ID', width: 70},
{field: 'ticket_code', title: '票码', width: 200},

View File

@ -0,0 +1,102 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>核销员管理 - VR票务</title>
{include file="public/head" /}
</head>
<body>
<div class="layui-fluid">
<div class="layui-card">
<div class="layui-card-header">核销员管理</div>
<div class="layui-card-body">
<!-- 搜索栏 -->
<div class="layui-form layui-form-pane">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">关键词</label>
<div class="layui-input-inline">
<input type="text" name="keywords" value="" placeholder="核销员名称/用户ID" class="layui-input">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">状态</label>
<div class="layui-input-inline">
<select name="status" lay-search>
<option value="">全部</option>
<option value="1">启用</option>
<option value="0">禁用</option>
</select>
</div>
</div>
<div class="layui-inline">
<button class="layui-btn" lay-submit lay-filter="search">搜索</button>
<a href="{:PluginsAdminUrl('vr_ticket', 'verifier', 'save')}" class="layui-btn layui-btn-normal">添加核销员</a>
</div>
</div>
</div>
<!-- 数据表格 -->
<table class="layui-hide" id="table" lay-filter="table"></table>
<script type="text/template" id="statusTpl">
{{# if (d.status == 1) { }}
<span class="layui-badge layui-bg-green">启用</span>
{{# } else { }}
<span class="layui-badge layui-bg-gray">禁用</span>
{{# } }}
</script>
<script type="text/template" id="actionTpl">
<a href="{:PluginsAdminUrl('vr_ticket', 'verifier', 'save')}?id={{d.id}}" class="layui-btn layui-btn-xs">编辑</a>
<a href="javascript:;" class="layui-btn layui-btn-danger layui-btn-xs" lay-fn="del" data-id="{{d.id}}">禁用</a>
</script>
</div>
</div>
</div>
{include file="public/footer" /}
<script>
layui.use('table', function() {
var table = layui.table;
var form = layui.form;
table.render({
elem: '#table',
url: '{:PluginsAdminUrl("vr_ticket", "verifier", "list")}',
cols: [[
{field: 'id', title: 'ID', width: 80},
{field: 'name', title: '核销员名称', minWidth: 120},
{field: 'user_id', title: '关联用户ID', width: 120},
{field: 'user_name', title: '用户昵称', width: 150},
{field: 'status', title: '状态', width: 100, templet: '#statusTpl'},
{field: 'created_at', title: '创建时间', width: 180, templet: function(d) {
return d.created_at > 0 ? layui.util.toDateString(d.created_at * 1000) : '-';
}},
{field: 'action', title: '操作', width: 180, templet: '#actionTpl'},
]]
});
form.on('submit(search)', function(data) {
table.reload('table', {where: data.field, page: {curr: 1}});
return false;
});
$(document).on('click', '[lay-fn="del"]', function() {
var id = $(this).data('id');
layer.confirm('确认禁用该核销员?', function(index) {
$.post('{:PluginsAdminUrl("vr_ticket", "verifier", "delete")}', {id: id}, function(res) {
if (res.code == 0) {
layer.msg('操作成功');
table.reload('table');
} else {
layer.msg(res.msg || '操作失败');
}
});
layer.close(index);
});
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,79 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>核销员 - VR票务</title>
{include file="public/head" /}
</head>
<body>
<div class="layui-fluid">
<div class="layui-card">
<div class="layui-card-header">{$info.id > 0 ? '编辑' : '添加'}核销员</div>
<div class="layui-card-body">
<form class="layui-form layui-form-pane" lay-filter="form">
<input type="hidden" name="id" value="{$info.id|default=0}">
<div class="layui-form-item">
<label class="layui-form-label">关联用户 <span style="color:red">*</span></label>
<div class="layui-input-inline" style="width:400px;">
<select name="user_id" lay-search lay-verify="required" {$info.id > 0 ? 'disabled' : ''}>
<option value="">请选择用户</option>
{foreach $users as $u}
<option value="{$u.id}" {if isset($info.user_id) && $info.user_id == $u.id}selected{/if}>
{$u.id} - {$u.nickname|default=$u.username} {$u.username ? '(' . $u.username . ')' : ''}
</option>
{/foreach}
</select>
</div>
{if $info.id > 0}
<div class="layui-form-mid layui-word-aux">用户关联后不可更改,如需变更请禁用后重新添加</div>
{/if}
</div>
<div class="layui-form-item">
<label class="layui-form-label">核销员名称 <span style="color:red">*</span></label>
<div class="layui-input-inline" style="width:400px;">
<input type="text" name="name" value="{$info.name|default=''}" lay-verify="required" placeholder="请输入核销员名称" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-inline">
<input type="checkbox" name="status" value="1" lay-skin="switch" lay-text="启用|禁用" {if empty($info.status) || $info.status == 1}checked{/if}>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="submit">提交</button>
<a href="{:PluginsAdminUrl('vr_ticket', 'verifier', 'list')}" class="layui-btn layui-btn-primary">返回列表</a>
</div>
</div>
</form>
</div>
</div>
</div>
{include file="public/footer" /}
<script>
layui.use('form', function() {
var form = layui.form;
form.on('submit(submit)', function(data) {
data.field.status = data.field.status ? 1 : 0;
$.post('{:PluginsAdminUrl("vr_ticket", "verifier", "save")}', data.field, function(res) {
if (res.code == 0) {
layer.msg('保存成功', function() {
location.href = '{:PluginsAdminUrl("vr_ticket", "verifier", "list")}';
});
} else {
layer.msg(res.msg || '保存失败');
}
});
return false;
});
});
</script>
</body>
</html>