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

11 KiB
Raw Permalink Blame History

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

$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 行:

{{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支持主题覆盖插件文件

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.htmlShopXO 默认主题目录),然后修改。但这是覆盖核心文件,升级 ShopXO 时会丢失。

推荐:不要完全替换页面。 改为在 base tab 内注入 ticket 专属表单,或添加新的 tab 项。


Q2-C: Save() 数据接收方式

Goods::Save()Goods.php:187-192

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

// 构建 $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 添加:

    "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.phpadmin控制器中不存在 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 已使用原始 textareasave.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.htmllayui 表单 + 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 库(过度工程化)