vr-shopxo-plugin/council-output/EDITOR_RESEARCH.md

266 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# vr-shopxo-plugin 编辑器方案调研报告
> 版本v1.0 | 日期2026-04-15 | AgentBackendArchitect (Q2) + FrontendDev (Q1)
## Q2商品发布页替换方案邪门方案可行性 — BackendArchitect
### 核心代码路径
| 文件 | 作用 |
|------|------|
| `shopxo/app/admin/controller/Goods.php:82-177` | SaveInfo() 方法 |
| `shopxo/app/admin/controller/Goods.php:187-192` | Save() 方法 |
| `shopxo/app/service/GoodsService.php:1549-1565` | plugins_service_goods_save_handle 钩子 |
| `shopxo/app/admin/view/default/goods/saveinfo.html:505-510` | 钩子渲染位置 |
---
### Q2-A: 钩子调用位置分析
**`plugins_view_admin_goods_save` 在 SaveInfo() 中的位置**Goods.php:159-167
```php
$hook_name = 'plugins_view_admin_goods_save';
$assign[$hook_name.'_data'] = MyEventTrigger($hook_name, [
'hook_name' => $hook_name,
'is_backend' => true,
'goods_id' => isset($params['id']) ? $params['id'] : 0,
'data' => &$data,
'params' => &$params,
]);
// 紧接着:
MyViewAssign($assign);
return MyView(); // 渲染 saveinfo.html
```
**结论:钩子在模板渲染之前被调用,结果存入 `$assign['plugins_view_admin_goods_save_data']` 供模板使用。**
---
### Q2-B: 能否完全替换页面内容?
**关键发现NO — 钩子仅是注入点,不是替换点。**
查看 `saveinfo.html` 模板结构(简化):
```
ModuleInclude('public/header')
<div class="content">
<form action="admin/goods/save" method="POST">
[商品名称输入框]
[商品分类选择器]
[nav_switch_btn: base/spec/parameters/photos/content/video/seo/use_guide]
<!-- base tab 内容 -->
<div class="am-form-group">
<label>...</label>
<div>
plugins_view_admin_goods_save ← 钩子在此注入
</div>
</div>
[SEO信息tab]
[popup submit按钮]
</form>
</div>
```
钩子注入位置在第 505-510 行:
```html
{{if !empty($plugins_view_admin_goods_save_data) and is_array(...)}}
{{foreach $plugins_view_admin_goods_save_data as $hook}}
{{$hook|raw}}
{{/foreach}}
{{else /}}
{{:ModuleInclude('public/not_data')}}
{{/if}}
```
**注入内容受限于 `<div class="am-form-group">` 容器内**,外层 `<form>`、Tab 导航、商品名称/分类等核心字段无法被替换。
### 替代方案:模板文件覆盖
`MyView()` 函数common.php:984-991支持主题覆盖插件文件
```php
if(substr($view, 0, 16) == '../../../plugins') {
$plugins_view_file = APP_PATH.$group.DS.'view'.DS.$theme.DS.'plugins'.DS.str_replace(...);
if(@file_exists($plugins_view_file)) {
$view = $plugins_view_file; // 主题文件覆盖插件文件
}
}
```
但这只对 `plugins` 控制器的路径生效。SaveInfo() 是 `admin/goods` 路径,无法利用此机制。
**唯一可行的完全替换路径**:将 `saveinfo.html` 复制到 `app/admin/view/default/goods/saveinfo.html`ShopXO 默认主题目录),然后修改。但这是覆盖核心文件,升级 ShopXO 时会丢失。
**推荐:不要完全替换页面。** 改为在 base tab 内注入 ticket 专属表单,或添加新的 tab 项。
---
### Q2-C: Save() 数据接收方式
**Goods::Save()Goods.php:187-192**
```php
public function Save() {
$params = $this->data_request; // ← ThinkPHP 标准请求数据($_POST
$params['admin'] = $this->admin;
return ApiService::ApiDataReturn(GoodsService::GoodsSave($params));
}
```
数据源是 ThinkPHP 的 `$this->data_request`,等价于标准 `$_POST`。**任何自定义表单都可以 POST 到 `admin/goods/save`,只要字段名符合 GoodsService 期望。**
**GoodsService::GoodsSave() 中钩子位置GoodsService.php:1549-1565**
```php
// 构建 $data 数组(从 $params 提取 title, category_ids 等)
$data['title'] = $params['title'] ?? '';
// ... 更多字段 ...
// 商品保存处理钩子 — 在事务启动之前
$ret = EventReturnHandle(MyEventTrigger('plugins_service_goods_save_handle', [
'params' => &$params, // 引用:可修改
'data' => &$data, // 引用:可修改(影响最终 INSERT/UPDATE
'spec' => &$specifications['data'],
'goods_id' => isset($params['id']) ? intval($params['id']) : 0,
]));
if(isset($ret['code']) && $ret['code'] != 0) {
return $ret; // ← 钩子可提前返回,阻止标准保存
}
```
**关键能力**
1. `$data` 数组通过引用传入,插件可修改后影响最终 `INSERT/UPDATE`
2. 钩子返回 `['code'=>0, 'msg'=>'...', 'data'=>...]` 可阻止标准流程(直接返回)
3.`$params['title']`、`$params['category_ids']` 等字段在钩子调用前已被提取进 `$data`
**两条可行路径**
- **路径A推荐**:在 `$data` 中填入最小必需字段title、category_ids 等),让标准 INSERT 继续执行,插件在钩子内完成票务数据保存
- **路径B**:钩子直接 `Db::startTrans()` 自己处理票务数据,然后 `return ['code'=>0]` 阻止标准流程
---
### Q2-D: 插件视图文件路径可行性
目前 `plugin.json` 中未注册 `plugins_view_admin_goods_save` 钩子(只有 `onOrderPaid`)。需要两步启用:
1. **注册钩子**:在 `plugin.json` 添加:
```json
"backend_hook": {
"plugins_view_admin_goods_save": ["\\app\\plugins\\vr_ticket\\hook\\AdminGoodsSave"]
}
```
2. **实现 AdminGoodsSave.php**:返回 HTML 字符串,注入到 saveinfo.html 的 base tab
**插件视图文件路径**`plugins/vr_ticket/view/admin/goods/ticket_save.html`(如果走模板覆盖方案)
---
### item_type 字段验证
- `goods` 表已存在 `item_type` 字段EventListener.php:129值包括 `'normal'` / `'ticket'` / `'physical'`
- 前台 `app/index/controller/Goods.php:139` 已有 `item_type == 'ticket'` 判断
- **后台 `Goods.php`admin控制器中不存在 `item_type` 判断逻辑**——任务描述中关于此判断存在于后台的说法需要更正
---
## Q1JSON 编辑器复杂度评估FrontendDev
### Q1.1: ShopXO DIY 组件中是否有现成 JSON 编辑器?
**结论ShopXO DIY 编辑器为商业闭源组件,无公开源码,无 JSON 编辑能力。**
调研过程:
- `shopxo/public/static/diy/js/entry/index-d71f3dad.js` — 预构建的单个大型压缩包4MB+ CSS + 完整 Vue3 运行时),无可读源码
- `shopxo/public/static/diy/js/chunk/` — 仅含 3 个空壳 JS 文件,总计 ~5 行代码
- `shopxo/sourcecode/index.html` — 空占位文件ShopXO 源码包需从其商业平台单独获取)
- `shopxo/app/admin/view/default/diy/saveinfo.html` — 仅一行 `ModuleInclude`,无组件定义
**ShopXO DIY 编辑器是页面可视化装修工具**(拖拽组件排版),不是通用 JSON 编辑器。其 `custom` 组件类型支持输出 HTML 代码片段,不可复用。
---
### Q1.2: Vue3 + JSON Schema Form 编辑器复杂度估算
**现有插件的起点**SeatTemplate 已使用**原始 textarea**`save.html:40`)直接编辑 seat_map JSON无任何可视化。
**引入"场馆"后的嵌套结构**
```
venue { name, address, image }
└─ seat_map
├─ map[] string[]
├─ seats{} { [key]: { price, color, label, classes? } }
├─ row_labels[] string[]
├─ sections[] { name, color }[]
└─ zones{} { [key]: { ... } }
```
实际嵌套深度:**3 层**venue → seat_map → seats/sections。不是"4层"venue 和 seat_map 各算一层。
**实现方式对比**
| 方案 | 代码量 | 工时 | 难度 | 可维护性 |
|------|--------|------|------|----------|
| **原始 textarea**(现状) | 0 | 0 | 无 | 差(无校验) |
| **表单可视化编辑器** | ~500行 Vue3 | ~1-1.5人天 | 中 | 好 |
| **JSON Schema + 校验** | ~1000行 Vue3 | ~2人天 | 高 | 好(需维护 schema |
**推荐方案:表单可视化编辑器(中等方案)**
- venue 信息独立字段text + image upload
- seat_map.map[]:动态行编辑器(每行一个 input支持增删
- seat_map.seats{}:键值对表格(行标签 → {price/color/label}
- seat_map.sections[]:卡片式列表(每区一张 card含 color picker
- 无需引入 JSON Schema 库,直接写 Vue3 render function
**技术实现路径**
1. 创建 `plugins/vr_ticket/admin/view/goods/ticket_editor.html`layui 表单 + Vue3 CDN
2. 通过 `AdminGoodsSave.php` hook 注入到商品发布页 base tab
3. seat_map 作为单个 JSON 字段存储(不做拆表)
---
### Q1.3: JSON 编辑器 vs 拆表方案成本对比
**JSON 单表方案(推荐)**
- 数据存储1 个 `seat_map` JSON 字段在 `plugins_vr_seat_templates`
- 编辑器:自定义 Vue3 表单500行代码1-1.5人天
- 优点schema 演进灵活(加字段不影响表结构),无 JOIN 查询
- 缺点:无数据库级数据完整性约束,查询特定座位需 JSON 函数
**拆多表方案**
```
plugins_vr_venues (venue 信息)
└─ plugins_vr_seat_templates (seat_map FK)
├─ plugins_vr_seat_sections (座位区:区名/颜色)
└─ plugins_vr_seat_zone_mappings (座位字符→区的映射)
```
- 代码量:需建 3-4 张表 + 4 套 CRUD + 模型关联,~1000行 PHP~800行前端
- 工时:~2.5-3人天远超 JSON 方案)
- 优点:数据库约束强,适合超大规模(万级座位)
- 缺点:表结构变更成本高;这个场景下收益有限
**结论**vr_ticket 插件的座位数通常 < 1000JSON 单表方案**开发和维护成本均显著低于拆表方案**。拆表仅在需要单独查询座位库存、索引、或座位有独立业务属性时才值得。
---
## 最终推荐
### 推荐方案:**钩子注入 + 表单可视化编辑器(中等方案)**
**综合 Q1 + Q2 结论**
| 评估维度 | 结论 |
|----------|------|
| 页面替换(邪门方案)| **不可行** 钩子仅注入非替换,需覆盖核心模板失去升级兼容性 |
| 独立路由方案 | 可行但工作量大(~3人天) |
| **钩子注入 + JSON 表单编辑器** | **推荐** 1-1.5人天,升级兼容,数据可控 |
| JSON vs 拆表 | **JSON 单表** 开发成本低 50%+,维护简单 |
**具体实施路径**
1. `plugin.json` 注册 `plugins_view_admin_goods_save` 钩子
2. 实现 `AdminGoodsSave.php` 返回 ticket 专属表单 HTMLlayui + Vue3 CDN
3. 表单编辑器结构:`venue 字段组` + `seat_map 可视化编辑器`(非 textarea
4. `plugins_service_goods_save_handle` 钩子接收并处理 ticket 数据(引用 `$data`
5. `item_type='ticket'` 时前端走 `ticket_detail.html`(已有实现)
**不推荐**
- 完全替换 saveinfo.html(失去升级兼容性)
- 拆多表(收益<成本)
- 引入 JSON Schema 库(过度工程化)