# shopxo-uniapp 前端编译与自定义

> 调研时间：2026-04-14
> 源码位置：`council-research/shopxo-eval/.worktrees/shopxo-evaluator/shopxo-uniapp-src/`
> 官方仓库：https://gitee.com/zongzhige/shopxo-uniapp

---

## 一、项目概述

shopxo-uniapp 是 ShopXO 官方出品的 uni-app 前端，支持：
- ✅ 微信小程序（MP-WEIXIN）
- ✅ QQ 小程序（MP-QQ）
- ✅ 百度小程序（MP-BAIDU）
- ✅ 支付宝小程序（MP-ALIPAY）
- ✅ 抖音/头条小程序（MP-TOUTIAO）
- ✅ 快手小程序（MP-KUAISHOU）
- ✅ H5
- ✅ APP（iOS/Android）

**README 原文**：> 已支持小程序（微信、QQ、百度、支付宝、头条&抖音、快手）+ H5 + APP

---

## 二、编译到微信小程序

### 2.1 编译工具

**HBuilderX**（uni-app 官方 IDE）

### 2.2 编译步骤

1. 用 HBuilderX 导入 `shopxo-uniapp-src` 项目
2. 修改 `App.vue` 中的接口地址：
   ```javascript
   // globalData 配置
   request_url: 'https://your-shopxo-domain.com/',  // 你的 ShopXO 域名
   static_url: 'https://your-shopxo-domain.com/static/'
   ```
3. 修改 `manifest.json` 中的 AppID（微信小程序配置）：
   ```json
   {
     "mp-weixin": {
       "appid": "wx YOUR APPID",
       "setting": {
         "urlCheck": false
       }
     }
   }
   ```
4. 顶部菜单 → **发行** → **微信小程序**
5. 用微信开发者工具打开发行目录

### 2.3 条件编译指令

在 `.vue` 文件和 `pages.json` 中使用：

```vue
<!-- #ifdef MP-WEIXIN || MP-BAIDU || MP-QQ || MP-KUAISHOU || H5 || APP -->
<view class="top-nav">微信/百度/QQ/快手/H5/APP 专属内容</view>
<!-- #endif -->

<!-- #ifdef MP-ALIPAY -->
<view>支付宝专属内容</view>
<!-- #endif -->

<!-- #ifndef H5 -->
<!-- 非 H5 平台（小程序）专属内容 -->
<uni-icons type="scan" @tap="scan_event"></uni-icons>
<!-- #endif -->
```

在 `pages.json` 中：
```json
{
  "path": "pages/goods-detail/goods-detail",
  "style": {
    "// #ifdef MP-WEIXIN": "",
    "navigationStyle": "custom",
    "// #endif": "",
  }
}
```

---

## 三、pages.json 路由与全局组件

### 3.1 核心页面

```json
{
  "pages": [
    { "path": "pages/index/index" },           // 首页（含 DIY）
    { "path": "pages/goods-category/goods-category" },  // 分类
    { "path": "pages/cart/cart" },             // 购物车
    { "path": "pages/user/user" }             // 用户中心
  ],
  "subPackages": [
    {
      "root": "pages/diy",                    // DIY 页面
      "pages": [{ "path": "diy" }]
    },
    {
      "root": "pages/goods-detail",          // 商品详情
      "pages": [{ "path": "goods-detail" }]
    },
    {
      "root": "pages/goods-search",           // 搜索
      "pages": [{ "path": "goods-search" }]
    },
    {
      "root": "pages/user-order-detail",      // 订单详情
      "pages": [{ "path": "user-order-detail" }]
    }
  ]
}
```

### 3.2 全局注册的自定义组件

在 `pages/index/index` 的 `style.usingComponents` 中全局注册：

```json
{
  "component-diy": "/pages/diy/components/diy/diy",
  "component-form-input": "/pages/form-input/components/form-input/form-input",
  "component-layout": "/pages/design/components/layout/layout",
  "component-goods-comments": "/pages/goods-detail/components/goods-comments/goods-comments",
  "component-coupon-card": "/pages/plugins/coupon/components/coupon-card/coupon-card",
  "component-form-input-base": "/pages/form-input/components/form-input/form-input-base"
}
```

其他页面按需引入。

---

## 四、商品详情页改造

### 4.1 商品详情页位置

`pages/goods-detail/goods-detail.vue`

### 4.2 添加票务条件渲染

在 `<template>` 中添加：

```vue
<template>
  <view :class="theme_view">
    <!-- 顶部导航（始终显示）-->
    <view class="page ...">
      <top-nav></top-nav>

      <!-- 票务商品：完全自定义页面结构 -->
      <block v-if="goods && goods.item_type === 'ticket'">
        <ticket-header :goods="goods"></ticket-header>
        <ticket-seat-selector
          :goods-id="goods.id"
          :spec-data="goods_spec_data"
          @select="on_seat_select"
        ></ticket-seat-selector>
        <ticket-info :goods="goods"></ticket-info>
        <ticket-attendee-form
          :max-count="selected_seats.length"
          @submit="on_attendee_submit"
        ></ticket-attendee-form>
        <ticket-purchase-bar
          :goods="goods"
          :selected-seats="selected_seats"
          :attendees="attendees"
          @buy="ticket_buy"
        ></ticket-purchase-bar>
      </block>

      <!-- 普通商品：原有完整 UI -->
      <block v-else>
        <goods-photo :photos="goods_photo"></goods-photo>
        <goods-price :goods="goods"></goods-price>
        <goods-spec-select
          v-if="goods.is_exist_many_spec == 1"
          :spec-data="goods_spec_data"
        ></goods-spec-select>
        <goods-buy-nav :buy-button="buy_button"></goods-buy-nav>
        <goods-tabs :tabs="tabs"></goods-tabs>
      </block>
    </view>
  </view>
</template>
```

### 4.3 后端配合（可选）

如果需要完全不同的页面结构，可以：

**方案 A：插件钩子替换**
- 在 `plugins_view_goods_detail_base_sku_top` 注入跳转按钮
- 点击后用 `uni.reLaunch` 跳转到 `pages/ticket-buy/ticket-buy`

**方案 B：修改 Goods.php 控制器（1 行）**
```php
// app/index/controller/Goods.php Index() 方法
if($goods['item_type'] == 'ticket') {
    return MyView('/goods/ticket_detail');  // 自定义模板
}
return MyView();
```

**推荐方案 A**（通过插件机制，不修改核心代码）。

---

## 五、新建票务专属页面

### 5.1 在 pages.json 中添加路由

```json
{
  "subPackages": [
    {
      "root": "pages/ticket-buy",
      "pages": [
        {
          "path": "ticket-buy",
          "style": {
            "navigationBarTitleText": "选择座位",
            "navigationStyle": "custom"
          }
        }
      ]
    },
    {
      "root": "pages/ticket-verify",
      "pages": [
        {
          "path": "ticket-verify",
          "style": {
            "navigationBarTitleText": "票务核销",
            "enablePullDownRefresh": false
          }
        }
      ]
    },
    {
      "root": "pages/ticket-wallet",
      "pages": [
        {
          "path": "ticket-wallet",
          "style": {
            "navigationBarTitleText": "我的票夹"
          }
        }
      ]
    }
  ]
}
```

### 5.2 页面目录结构

```
pages/ticket-buy/
├── ticket-buy.vue        # 选座 + 购票主流程
└── components/
    ├── seat-selector.vue # 座位选择器
    ├── attendee-form.vue  # 观演人表单
    └── purchase-bar.vue  # 购买栏

pages/ticket-wallet/
├── ticket-wallet.vue    # 票夹主页面
└── components/
    └── ticket-card.vue  # 电子票卡片（含 QR 码）

pages/ticket-verify/
└── ticket-verify.vue    # B 端核销页面（参考 realstore/check.vue）
```

---

## 六、uni.scanCode 扫码能力

### 6.1 基础用法

```javascript
// 扫码
uni.scanCode({
    onlyFromCamera: true,  // 只允许相机扫码
    success: (res) => {
        console.log('扫码结果:', res.result);
        // res.result: 扫码内容
        // res.scanType: 二维码类型（QR_CODE 等）
    },
    fail: (err) => {
        console.error('扫码失败', err);
    }
});
```

### 6.2 在 H5 平台的处理

```vue
<!-- #ifndef H5 -->
<uni-icons type="scan" size="56rpx" @tap="scan_event"></uni-icons>
<!-- #endif -->

<!-- H5 平台提示手动输入 -->
<!-- #ifdef H5 -->
<view class="cr-grey text-size-sm" @tap="show_manual_input">
  请输入核销码
</view>
<!-- #endif -->
```

### 6.3 参考实现

ShopXO 的 `realstore/check/check.vue` 已完整实现：
- 扫码触发 → 自动提交验证
- 手动输入兼容
- 成功/失败状态显示
- 连续扫描（核销后清空输入框）

---

## 七、QR 码展示

### 7.1 后端生成 QR 码图片 URL

```javascript
// 生成票务 QR 码 URL
function getTicketQrUrl(ticketId) {
    const baseUrl = app.globalData.request_url;
    const ticketData = JSON.stringify({
        id: ticketId,
        exp: Date.now() + 30 * 86400 * 1000  // 30天有效期
    });
    const encoded = encodeURIComponent(
        uni.base64ToString(
            uni.stringToBase64(ticketData)
        )
    );
    return `${baseUrl}?s=index/qrcode/index&content=${encoded}&size=8&level=H`;
}
```

### 7.2 在页面中展示

```vue
<template>
  <view class="ticket-card">
    <image :src="qrCodeUrl" mode="aspectFit" class="qr-image" />
    <view class="ticket-code">{{ ticket.code }}</view>
  </view>
</template>
```

---

## 八、manifest.json 关键配置

### 8.1 多端配置

```json
{
  "name": "ShopXO",
  "appid": "__UNI__50E3C11",
  "description": "ShopXO开源商城...",
  "transformPx": false,
  "app-plus": {
    "usingComponents": true,
    "nvueCompiler": "uni-app",
    "distribute": {
      "android": { ... },
      "ios": { ... },
      "mp-weixin": {
        "appid": "wx YOUR APPID",
        "setting": {
          "urlCheck": false,
          "es6": true,
          "minified": true
        },
        "usingComponents": true
      }
    }
  }
}
```

### 8.2 权限配置（微信小程序）

```json
"mp-weixin": {
  "permission": {
    "scope.userLocation": {
      "desc": "用于场馆定位"
    }
  }
}
```

---

## 九、自定义组件注册

### 9.1 全局注册（所有页面可用）

在 `pages/index/index.vue` 的 `style.usingComponents` 中添加：

```json
"component-ticket-seat": "/pages/ticket-buy/components/seat-selector/seat-selector",
"component-ticket-card": "/pages/ticket-wallet/components/ticket-card/ticket-card",
"component-attendee-form": "/pages/ticket-buy/components/attendee-form/attendee-form"
```

### 9.2 页面级注册

在单个页面的 `pages.json` 中：
```json
{
  "path": "pages/ticket-wallet/ticket-wallet",
  "style": {
    "usingComponents": {
      "component-ticket-card": "/pages/ticket-wallet/components/ticket-card/ticket-card"
    }
  }
}
```

---

## 十、ShopXO API 调用

### 10.1 请求封装

ShopXO uni-app 使用全局封装的请求方法：

```javascript
uni.request({
    url: app.globalData.get_request_url('action', 'controller', 'module'),
    method: 'POST',
    data: { ... },
    success: (res) => {
        if (res.data.code == 0) {
            // 成功
        } else {
            // 失败
        }
    }
});
```

### 10.2 插件 API 路径

对于插件的 API：
```javascript
// 格式：get_request_url(action, controller, plugins_name)
const url = app.globalData.get_request_url(
    'verify',        // action
    'adminverify',   // controller
    'vrticket'       // 插件名（apps 目录下的目录名）
);
```

### 10.3 鉴权

请求自动带上登录态：
```javascript
// 已在全局封装，每次请求自动附加：
// Header: Authorization / Token
// 或者通过 Session/Cookie
```
