472 lines
11 KiB
Markdown
472 lines
11 KiB
Markdown
|
|
# 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
|
|||
|
|
```
|