diff --git a/shopxo/app/plugins/vr_ticket/admin/controller/Venue.php b/shopxo/app/plugins/vr_ticket/admin/controller/Venue.php new file mode 100644 index 0000000..103db5a --- /dev/null +++ b/shopxo/app/plugins/vr_ticket/admin/controller/Venue.php @@ -0,0 +1,318 @@ +where($where) + ->order('id', 'desc') + ->paginate(20) + ->toArray(); + + // 解析 venue.name 和座位数 + foreach ($list['data'] as &$item) { + $seatMap = json_decode($item['seat_map'] ?? '{}', true); + // v3.0: venue.name 优先,否则降级取模板 name + $item['venue_name'] = $seatMap['venue']['name'] ?? $item['name']; + $item['venue_address'] = $seatMap['venue']['address'] ?? ''; + $item['zone_count'] = !empty($seatMap['sections']) ? count($seatMap['sections']) : 0; + $item['seat_count'] = self::countSeats($seatMap); + } + unset($item); + + return view('', [ + 'list' => $list['data'], + 'page' => $list['page'], + 'count' => $list['total'], + ]); + } + + /** + * 添加/编辑场馆 + * + * @note v3.0 seat_map JSON 结构: + * { + * "venue": { "name": "...", "address": "...", "image": "..." }, + * "map": ["AAAAAA", "BBBBBB"], + * "seats": { "A": { "price": 899, "color": "#e74c3c", "label": "VIP区" }, ... }, + * "sections": [{ "char": "A", "name": "VIP区", "color": "#e74c3c" }, ...], + * "row_labels": ["A", "B"] + * } + */ + public function save() + { + $id = input('id', 0, 'intval'); + + if (IS_AJAX_POST) { + // 接收表单数据 + $data = [ + 'name' => input('name', '', null, 'trim'), + 'category_id' => input('category_id', 0, 'intval'), + 'status' => input('status', 1, 'intval'), + 'upd_time' => time(), + ]; + + // 验证必填字段 + if (empty($data['name'])) { + return DataReturn('场馆名称不能为空', -1); + } + + // v3.0: 构建 seat_map JSON(venue 顶层嵌入) + $venue = [ + 'name' => input('venue_name', '', null, 'trim'), + 'address' => input('venue_address', '', null, 'trim'), + 'image' => input('venue_image', '', null, 'trim'), + ]; + if (empty($venue['name'])) { + return DataReturn('场馆名称不能为空', -1); + } + + // 解析分区配置(前端传来的 JSON) + $zones_raw = input('zones', '[]', null, 'trim'); + $zones = json_decode($zones_raw, true); + if (!is_array($zones) || empty($zones)) { + return DataReturn('请至少添加一个分区', -1); + } + + // 解析座位排布(每排座位数字符串数组) + $map_raw = input('seat_map_rows', '[]', null, 'trim'); + $map = json_decode($map_raw, true); + if (!is_array($map) || empty($map)) { + return DataReturn('座位排布不能为空', -1); + } + + // 构建 sections(分区元信息) + $sections = []; + $seats = []; // 每行的默认元信息 + $row_labels = []; + foreach ($zones as $zone) { + $char = $zone['char'] ?? ''; + if (empty($char)) { + return DataReturn('分区字符不能为空', -1); + } + $sections[] = [ + 'char' => strtoupper($char), + 'name' => $zone['name'] ?? '', + 'color' => $zone['color'] ?? '#cccccc', + ]; + $seats[strtoupper($char)] = [ + 'price' => intval($zone['price'] ?? 0), + 'color' => $zone['color'] ?? '#cccccc', + 'label' => $zone['name'] ?? '', + ]; + } + + // 构建 row_labels + $row_labels = array_unique(array_column($sections, 'char')); + + // 验证:每排座位数与 char 匹配 + foreach ($map as $rowStr) { + $rowStr = trim($rowStr); + if (empty($rowStr)) { + return DataReturn('座位排布每行不能为空', -1); + } + // 每行的每个字符必须是已定义的 zone char + foreach (str_split($rowStr) as $char) { + if ($char !== '_' && !isset($seats[$char])) { + return DataReturn("座位排布中字符 '{$char}' 未在分区中定义", -1); + } + } + $row_labels[] = $rowStr[0]; // 第一字符作为行标签 + } + + // 组装 seat_map v3.0 结构 + $data['seat_map'] = json_encode([ + 'venue' => $venue, + 'map' => $map, + 'seats' => $seats, + 'sections' => $sections, + 'row_labels' => array_values(array_unique($row_labels)), + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + + if ($id > 0) { + $data['upd_time'] = time(); + \Db::name('plugins_vr_seat_templates')->where('id', $id)->update($data); + return DataReturn('更新成功', 0, ['url' => MyUrl('plugins_vr_ticket/admin/venue/list')]); + } else { + $data['add_time'] = time(); + $data['upd_time'] = time(); + $data['spec_base_id_map'] = ''; + \Db::name('plugins_vr_seat_templates')->insert($data); + return DataReturn('添加成功', 0, ['url' => MyUrl('plugins_vr_ticket/admin/venue/list')]); + } + } + + // 编辑时加载数据 + $info = []; + if ($id > 0) { + $row = \Db::name('plugins_vr_seat_templates')->find($id); + if (!empty($row)) { + $seatMap = json_decode($row['seat_map'] ?? '{}', true); + $row['venue_name'] = $seatMap['venue']['name'] ?? ''; + $row['venue_address'] = $seatMap['venue']['address'] ?? ''; + $row['venue_image'] = $seatMap['venue']['image'] ?? ''; + $row['zones_json'] = json_encode($seatMap['sections'] ?? [], JSON_UNESCAPED_UNICODE); + $row['map_json'] = json_encode($seatMap['map'] ?? [], JSON_UNESCAPED_UNICODE); + $info = $row; + } + } + + // 加载分类列表 + $categories = \Db::name('GoodsCategory') + ->where('is_delete', 0) + ->order('id', 'asc') + ->select(); + + return view('', [ + 'info' => $info, + 'categories' => $categories, + ]); + } + + /** + * 删除场馆(软删除) + */ + public function delete() + { + if (!IS_AJAX_POST) { + return DataReturn('非法请求', -1); + } + + $id = input('id', 0, 'intval'); + if ($id <= 0) { + return DataReturn('参数错误', -1); + } + + $template = \Db::name('plugins_vr_seat_templates')->where('id', $id)->find(); + \Db::name('plugins_vr_seat_templates') + ->where('id', $id) + ->update(['status' => 0, 'upd_time' => time()]); + + // 审计日志 + \app\plugins\vr_ticket\service\AuditService::log( + \app\plugins\vr_ticket\service\AuditService::ACTION_DISABLE_TEMPLATE, + \app\plugins\vr_ticket\service\AuditService::TARGET_TEMPLATE, + $id, + ['before_status' => $template['status'] ?? 1], + $template ? "场馆: {$template['name']}" : "ID:{$id}" + ); + + return DataReturn('删除成功', 0); + } + + /** + * 预览 seat_map JSON(用于调试) + * GET ?s=plugins/vr_ticket/admin/venue/preview + */ + public function preview() + { + if (!IS_AJAX_POST && !IS_GET) { + return DataReturn('非法请求', -1); + } + + $zones_raw = input('zones', '[]', null, 'trim'); + $map_raw = input('seat_map_rows', '[]', null, 'trim'); + + $zones = json_decode($zones_raw, true); + $map = json_decode($map_raw, true); + + if (!is_array($zones)) { + return DataReturn('zones JSON 格式错误', -1); + } + if (!is_array($map)) { + return DataReturn('seat_map_rows JSON 格式错误', -1); + } + + // 构建预览用 seat_map + $sections = []; + $seats = []; + foreach ($zones as $zone) { + $char = strtoupper($zone['char'] ?? ''); + $sections[] = [ + 'char' => $char, + 'name' => $zone['name'] ?? '', + 'color' => $zone['color'] ?? '#cccccc', + ]; + $seats[$char] = [ + 'price' => intval($zone['price'] ?? 0), + 'color' => $zone['color'] ?? '#cccccc', + 'label' => $zone['name'] ?? '', + ]; + } + + $seat_map = [ + 'venue' => [ + 'name' => input('venue_name', '未命名场馆', null, 'trim'), + 'address' => input('venue_address', '', null, 'trim'), + 'image' => input('venue_image', '', null, 'trim'), + ], + 'map' => $map, + 'seats' => $seats, + 'sections' => $sections, + 'row_labels' => array_values(array_unique( + array_merge( + array_column($sections, 'char'), + array_map(fn($r) => $r[0] ?? '', $map) + ) + )), + ]; + + $seat_count = self::countSeats($seat_map); + + return DataReturn('预览生成成功', 0, [ + 'seat_map' => json_encode($seat_map, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), + 'seat_count' => $seat_count, + 'zone_count' => count($sections), + ]); + } + + /** + * 统计座位数 + */ + private static function countSeats($seatMap) + { + if (empty($seatMap) || empty($seatMap['seats']) || empty($seatMap['map'])) { + return 0; + } + $count = 0; + foreach ($seatMap['map'] as $row) { + foreach (str_split($row) as $char) { + if ($char !== '_' && isset($seatMap['seats'][$char])) { + $count++; + } + } + } + return $count; + } +} diff --git a/shopxo/app/plugins/vr_ticket/admin/view/venue/list.html b/shopxo/app/plugins/vr_ticket/admin/view/venue/list.html new file mode 100644 index 0000000..02f4f51 --- /dev/null +++ b/shopxo/app/plugins/vr_ticket/admin/view/venue/list.html @@ -0,0 +1,113 @@ + + +
+