Bläddra i källkod

添加了自定义段落组件

shenwu 4 dagar sedan
förälder
incheckning
63c5b0aa5c

+ 5 - 1
config/index.ts

@@ -16,6 +16,10 @@ const config = {
   },
   copy: {
     patterns: [
+      { 
+        from: 'src/components/novelPlugin/customParagraph', 
+        to: 'dist/components/novelPlugin/customParagraph' 
+      }
     ],
     options: {
     }
@@ -85,7 +89,7 @@ const config = {
   }
 }
 
-module.exports = function (merge) {
+module.exports = function (merge:any) {
   if (process.env.NODE_ENV === 'development') {
     return merge({}, config, require('./dev'))
   }

+ 3 - 3
src/app.config.ts

@@ -1,4 +1,4 @@
-/**全局配置https://taro-docs.jd.com/taro/docs/tutorial#%E5%85%A5%E5%8F%A3%E6%96%87%E4%BB%B6 */
+/**全局配置 https://taro-docs.jd.com/taro/docs/tutorial#%E5%85%A5%E5%8F%A3%E6%96%87%E4%BB%B6 */
 export default {
   pages: [
     'pages/index/index',//书城
@@ -57,7 +57,7 @@ export default {
       "provider": "wx293c4b6097a8a4d0",
       "genericsImplementation": {
         "novel": {
-         
+          "paragraph-block": "components/novelPlugin/customParagraph/index"
         }
       }
     },
@@ -66,4 +66,4 @@ export default {
       "provider": "wx104a1a20c3f81ec2"
     }
   }
-}
+}

+ 13 - 3
src/app.tsx

@@ -86,6 +86,16 @@ class App extends Component {
         },
       ],
     })
+    //自定义段落
+    novelManager.setParagraphBlock({
+      globalConfigs: [ // 在这里设置的是全局设置 会在所有章节生效
+        {
+          height: 200,//自定义段落高度,单位 px
+          position: 9999,//自定义段落位置,0 表示在标题前,1 表示在第一段文字前,以此类推
+          ext: JSON.stringify({url:"https://corp-msg.oss-cn-hangzhou.aliyuncs.com/17738993366268346745849EF42B39D9459749C6B2784.jpg"}),//透传给自定义组件的参数
+        },
+      ]
+    })
     // 监听用户行为事件
     novelManager.onUserTriggerEvent(res => {
       /**
@@ -128,7 +138,7 @@ class App extends Component {
           break;
         case "get_chapter":
           // 此处做用户阅读记录
-          console.log("拉取章节数据结束",res)
+          console.log("拉取章节数据结束", res)
           break;
         case "click_startread":
           console.log("点击了开始阅读")
@@ -138,7 +148,7 @@ class App extends Component {
           console.log("切换章节")
           break;
         case "change_page":
-          console.log("翻页",res)
+          console.log("翻页", res)
           break;
         case "leave_readpage":
           console.log("离开阅读页")
@@ -146,7 +156,7 @@ class App extends Component {
         case "close_ad":
           console.log("关闭广告")
           break;
-        case  "ad_error":
+        case "ad_error":
           console.log("广告报错")
           break;
       }

+ 723 - 0
src/components/novelPlugin/README.md

@@ -0,0 +1,723 @@
+# 微信官方阅读器插件 - 自定义段落组件
+
+## ⚠️ 重要说明
+
+**本组件使用微信原生格式(.wxml, .wxss, .js, .json),而非 Taro/React 格式**
+
+原因:
+- 微信官方阅读器插件的抽象节点组件必须是微信原生组件
+- Taro 编译后的 React组件无法被插件正确识别
+- 因此需要使用 `.wxml`, `.wxss`, `.js`, `.json` 原生文件格式
+
+## 概述
+
+该组件用于在微信官方阅读器插件中,在每章内容的底部展示自定义内容(二维码图片)。
+
+## 功能特性
+
+- ✅ 在每章内容底部自动显示自定义二维码图片
+- ✅ 支持动态配置二维码 URL(通过 `ext` 参数)
+- ✅ **支持长按识别二维码**(微信原生功能)
+- ✅ 符合微信官方阅读器插件规范
+- ✅ 使用微信原生组件格式,确保兼容性
+
+## 配置说明
+
+### 1. app.config.ts 配置
+
+已在 `src/app.config.ts` 中添加了插件配置:
+
+``json
+{
+  "plugins": {
+    "novel-plugin": {
+      "version": "latest",
+      "provider": "wx293c4b6097a8a4d0",
+      "genericsImplementation": {
+        "novel": {
+          "custom-paragraph": "components/novelPlugin/customParagraph/index"
+        }
+      }
+    }
+  }
+}
+```
+
+**注意**:路径是相对于小程序根目录(dist)的路径,不需要文件扩展名。
+
+### 2. 组件文件结构
+
+```
+src/components/novelPlugin/customParagraph/
+├── index.wxml    # 组件模板文件(微信原生)
+├── index.wxss    # 组件样式文件(微信原生)
+├── index.js      # 组件逻辑文件(微信原生)
+└── index.json    # 组件配置文件(微信原生)
+```
+
+### 3. 全局类型声明
+
+已在 `types/global.d.ts` 中添加了组件的类型声明,确保 TypeScript 能正确识别。
+
+## 组件属性
+
+阅读器插件会自动传入以下属性:
+
+| 参数 | 类型 | 描述 |
+| --- | --- | --- |
+| novelManagerId | Number | 阅读器实例 id |
+| bookId | String | 书籍 id |
+| chapterIndex | Number | 章节下标 |
+| chapterId | String | 章节 id |
+| paragraphIndex | Number | 段落下标 |
+| originalId | String | 章节主键 original_id |
+| **ext** | **String** | **透传参数(JSON 字符串格式),用于传递二维码 URL 等自定义数据** |
+
+## 📱 长按识别二维码功能
+
+### 功能说明
+
+组件已启用微信小程序的**长按识别二维码**功能。用户在阅读时:
+
+1. **长按**二维码图片(按住约 1 秒)
+2. 系统自动弹出菜单
+3. 选择"**识别图中二维码**"
+4. 即可跳转到对应的页面或小程序
+
+### 技术实现
+
+在 WXML 中添加了 `show-menu-by-longpress` 属性:
+
+```xml
+<image 
+  src="{{qrcodeUrl}}" 
+  class="qrcode-image"
+  mode="aspectFit"
+  show-menu-by-longpress="{{true}}"
+></image>
+```
+
+### 属性说明
+
+| 属性 | 值 | 说明 |
+|------|-----|------|
+| `show-menu-by-longpress` | `{{true}}` | 启用长按识别功能 |
+
+### 支持的二维码类型
+
+- ✅ **小程序码**:识别后打开对应小程序
+- ✅ **普通二维码**:识别后显示链接信息
+- ✅ **微信公众号二维码**:识别后关注公众号
+- ✅ **企业微信二维码**:识别后添加企业微信
+
+### 注意事项
+
+#### 1. 二维码图片要求
+
+- **格式**:建议使用 PNG 或 JPG 格式
+- **尺寸**:建议不小于 300x300 像素
+- **清晰度**:确保二维码清晰可识别
+- **对比度**:二维码与背景要有足够对比度
+
+#### 2. 用户体验建议
+
+```xml
+<!-- 提示文字 -->
+<text class="qrcode-tip">长按识别二维码</text>
+```
+
+**最佳实践:**
+- ✅ 在二维码下方显示"长按识别二维码"提示
+- ✅ 二维码大小适中(建议宽度 280-320rpx)
+- ✅ 留有足够的边距,避免误触
+- ✅ 放置在章节末尾,不影响阅读体验
+
+#### 3. 兼容性
+
+- ✅ 微信 iOS 版本 7.0.0+
+- ✅ 微信 Android 版本 7.0.0+
+- ✅ 所有支持的小程序基础库版本
+
+### 效果示例
+
+```
+┌─────────────────────────────┐
+│                             │
+│    [二维码图片]              │
+│                             │
+│   长按识别二维码             │
+│                             │
+└─────────────────────────────┘
+
+用户操作:
+1. 手指长按二维码  ────────────→
+2. 弹出系统菜单  ────────────→
+3. 选择"识别图中二维码" ───────→
+4. 跳转到目标页面/小程序        │
+```
+
+### 测试方法
+
+1. 在微信开发者工具中打开项目
+2. 使用真机预览(扫码预览到手机)
+3. 在阅读器插件中打开书籍
+4. 滚动到设置了二维码的段落
+5. 长按图中二维码测试识别功能
+
+### 常见问题
+
+#### Q: 为什么长按没有反应?
+
+**A:** 检查以下几点:
+1. 确认 `show-menu-by-longpress="{{true}}"` 已添加
+2. 必须是真机测试,模拟器可能不支持
+3. 确保按压力度足够,时间够长(约 1 秒)
+4. 检查微信版本是否支持此功能
+
+#### Q: 可以自定义菜单吗?
+
+**A:** 不可以。这是微信系统级别的菜单,无法自定义。用户长按后会看到微信默认的操作菜单。
+
+#### Q: 如何统计扫码数据?
+
+**A:** 可以在二维码 URL 中添加追踪参数:
+
+```javascript
+novelManager.setParagraphBlock({
+  chapterIndex: 0,
+  paragraphIndex: 5,
+  ext: JSON.stringify({
+    qrcodeUrl: 'https://example.com/qrcode?source=novel&chapter=0&bookId=xxx'
+  })
+})
+```
+
+然后在后台统计这些参数的访问数据。
+
+### 完整代码示例
+
+```javascript
+// app.js 或 app.tsx
+const novelPlugin = requirePlugin('novel-plugin')
+
+App({
+  onLaunch() {
+    novelPlugin.onPageLoad(this.onNovelPluginLoad)
+  },
+  
+  onNovelPluginLoad(data) {
+    const novelManager = novelPlugin.getNovelManager(data.id)
+    
+    // 设置带长按识别的二维码
+    novelManager.setParagraphBlock({
+      chapterIndex: 0,
+      paragraphIndex: 10,
+      ext: JSON.stringify({
+        qrcodeUrl: 'https://your-domain.com/qrcode.jpg'
+        // 这个二维码会被自动设置为可长按识别
+      })
+    })
+  }
+})
+```
+
+```xml
+<!-- components/novelPlugin/customParagraph/index.wxml -->
+<view class="custom-paragraph-container">
+  <view class="qrcode-box" wx:if="{{qrcodeUrl}}">
+    <image 
+      src="{{qrcodeUrl}}" 
+      class="qrcode-image"
+      mode="aspectFit"
+      show-menu-by-longpress="{{true}}"  <!-- 关键属性 -->
+    ></image>
+    <text class="qrcode-tip">长按识别二维码</text>
+  </view>
+</view>
+```
+
+---
+
+现在你的二维码已经支持**长按识别**功能了!用户只需长按二维码即可直接识别,无需截图或其他操作。🎉
+
+## 使用方法
+
+### 1. 在 app.tsx 中设置段落扩展信息
+
+使用 `NovelManager.setParagraphBlock()` 方法时,通过 `ext` 参数传递二维码 URL:
+
+```javascript
+// app.js 或 app.tsx
+const novelPlugin = requirePlugin('novel-plugin')
+
+App({
+  onLaunch() {
+    novelPlugin.onPageLoad(this.onNovelPluginLoad)
+  },
+  
+  onNovelPluginLoad(data) {
+    const novelManager = novelPlugin.getNovelManager(data.id)
+    
+    // 设置自定义段落块,通过 ext 传递二维码 URL
+    novelManager.setParagraphBlock({
+      chapterIndex: 0, // 章节下标
+      paragraphIndex: 5, // 段落下标(在第 6 段后显示)
+      ext: JSON.stringify({
+        qrcodeUrl: 'https://your-domain.com/your-qrcode.jpg'
+      })
+    })
+    
+    // 或者直接传递 URL 字符串
+    novelManager.setParagraphBlock({
+      chapterIndex: 0,
+      paragraphIndex: 5,
+      ext: 'https://your-domain.com/your-qrcode.jpg'
+    })
+  }
+})
+```
+
+### 2. ext 参数支持的格式
+
+组件支持以下三种 ext 格式:
+
+#### 格式 1:JSON 对象(推荐)
+```javascript
+ext: JSON.stringify({
+  qrcodeUrl: 'https://your-domain.com/qrcode.jpg'
+})
+```
+
+#### 格式 2:JSON 对象(其他字段名)
+```javascript
+ext: JSON.stringify({
+  imageUrl: 'https://your-domain.com/image.jpg'
+})
+// 或
+ext: JSON.stringify({
+  url: 'https://your-domain.com/link.jpg'
+})
+```
+
+#### 格式 3:直接传递 URL 字符串
+```javascript
+ext: 'https://your-domain.com/qrcode.jpg'
+```
+
+### 3. 组件解析逻辑
+
+组件会按以下顺序查找二维码 URL:
+
+1. 尝试解析 `ext` 为 JSON 对象
+2. 依次检查字段:`qrcodeUrl` → `imageUrl` → `url`
+3. 如果解析失败,检查是否以 `http` 开头,是则直接作为 URL 使用
+4. 如果都没有找到,不显示二维码
+
+### 4. 示例代码
+
+```
+// 示例 1:在指定章节后显示二维码
+novelManager.setParagraphBlock({
+  chapterIndex: 0,           // 第一章
+  paragraphIndex: 10,        // 第 11 段后
+  ext: JSON.stringify({
+    qrcodeUrl: 'https://example.com/qrcode1.jpg',
+    customData: '可以传递其他自定义数据'
+  })
+})
+
+// 示例 2:在多个位置显示不同的二维码
+novelManager.setParagraphBlock({
+  chapterIndex: 0,
+  paragraphIndex: 5,
+  ext: JSON.stringify({
+    qrcodeUrl: 'https://example.com/qrcode-a.jpg'
+  })
+})
+
+novelManager.setParagraphBlock({
+  chapterIndex: 0,
+  paragraphIndex: 15,
+  ext: JSON.stringify({
+    qrcodeUrl: 'https://example.com/qrcode-b.jpg'
+  })
+})
+
+// 示例 3:动态生成二维码 URL
+const bookId = 'book-123'
+const qrcodeUrl = `https://example.com/qrcode?bookId=${bookId}&chapter=0`
+
+novelManager.setParagraphBlock({
+  chapterIndex: 0,
+  paragraphIndex: 8,
+  ext: JSON.stringify({
+    qrcodeUrl: qrcodeUrl
+  })
+})
+```
+
+## 自定义修改
+
+### 修改二维码图片
+
+编辑 `src/components/novelPlugin/customParagraph/index.wxml` 文件中的图片地址:
+
+```
+<image 
+  src="你的二维码图片地址" 
+  class="qrcode-image"
+  mode="aspectFit"
+></image>
+```
+
+当前使用的图片地址:
+```
+https://corp-msg.oss-cn-hangzhou.aliyuncs.com/17738993366268346745849EF42B39D9459749C6B2784.jpg
+```
+
+### 修改提示文字
+
+编辑 `src/components/novelPlugin/customParagraph/index.wxml` 文件中的文字:
+
+```
+<text class="qrcode-tip">扫码了解更多</text>
+```
+
+### 修改样式
+
+编辑 `src/components/novelPlugin/customParagraph/index.wxss` 文件来自定义样式:
+
+- `.custom-paragraph-container` - 容器样式
+- `.qrcode-box` - 二维码容器样式
+- `.qrcode-image` - 二维码图片样式(注意使用 rpx 单位)
+- `.qrcode-tip` - 提示文字样式
+
+### 修改组件逻辑
+
+编辑 `src/components/novelPlugin/customParagraph/index.js` 文件来处理组件的生命周期和事件。
+
+## 使用说明
+
+1. 确保小程序已开通【文娱 - 小说】类目
+2. 确保已在微信公众平台配置小说阅读器插件权限
+3. **必须构建项目**:运行 `npm run build:weapp` 将 Taro 代码编译到 dist 目录
+4. 使用官方阅读器插件打开书籍时,自定义段落会自动显示在每章内容底部
+5. 无需额外代码调用,插件会自动加载该组件
+
+## ⚠️ 注意事项
+
+1. **组件格式**:必须使用微信原生格式(.wxml, .wxss, .js, .json),不能使用 Taro/React 格式
+2. **路径配置**:组件路径必须与 `app.config.ts` 中配置的路径一致
+3. **构建顺序**:先运行 `npm run build:weapp` 构建,然后在微信开发者工具中预览
+4. **文件位置**:Taro 会将 src 目录编译到 dist 目录,确保源文件在 src 目录下
+5. **单位使用**:样式文件中建议使用 rpx 单位以适配不同屏幕
+6. **组件优化**:确保组件不会被编译工具优化掉(抽象节点可能被误判为未使用)
+
+## 🚀 构建和测试步骤
+
+### 1. 构建项目
+
+由于使用了微信原生格式的组件,需要先构建项目:
+
+```bash
+# 进入项目目录
+cd /Users/shenwu/Desktop/gs-items/new-taro-mini-book
+
+# 构建微信小程序
+npm run build:weapp
+```
+
+### 2. 验证编译产物
+
+构建完成后,检查 `dist` 目录中是否生成了正确的文件:
+
+```bash
+cd dist/components/novelPlugin/customParagraph/
+# 应该看到以下文件:
+# - index.wxml
+# - index.wxss
+# - index.js
+# - index.json
+```
+
+### 3. 在微信开发者工具中预览
+
+1. 打开微信开发者工具
+2. 导入项目(如果已导入可跳过)
+3. 确保项目根目录设置为 `./dist`
+4. 编译项目(Cmd+B / Ctrl+B)
+5. 使用阅读器插件打开书籍
+
+### 4. 调试
+
+打开微信开发者工具的调试器:
+- 查看控制台输出,应该能看到 "自定义段落组件加载" 的日志
+- 查看 Wxml 面板,确认组件结构正确
+- 如果组件未显示,检查路径配置和文件是否存在
+
+## ❗ 常见问题
+
+### Q: 为什么报错 "Component is not found in path"?
+
+**A:** 可能的原因:
+1. 没有先运行 `npm run build:weapp` 构建项目
+2. 路径配置错误,确保是 `components/novelPlugin/customParagraph/index`
+3. 缺少必要的文件(.wxml, .wxss, .js, .json)
+4. Taro 配置问题,检查 app.config.ts 中的路径
+
+### Q: 为什么组件不显示?
+
+**A:** 检查以下几点:
+1. 是否在阅读器插件中使用(普通页面不会显示)
+2. 章节是否正确配置了状态(免费/付费)
+3. 图片地址是否可访问
+4. 样式是否有问题(如高度为 0)
+
+### Q: 可以使用 React/Taro语法吗?
+
+**A:** **不可以!** 抽象节点组件必须使用微信原生格式:
+- ❌ 不能使用 JSX/TSX
+- ❌ 不能使用 React hooks
+- ❌ 不能使用 Taro 组件
+- ✅ 只能使用 Component 构造器
+- ✅ 只能使用 .wxml, .wxss, .js, .json 文件
+
+## 为什么使用原生格式?
+
+微信官方阅读器插件的抽象节点组件(如 custom-paragraph)有特殊要求:
+
+1. **插件机制限制**:阅读器插件在加载抽象节点时,使用的是微信小程序的原生组件系统
+2. **编译时机**:Taro 的 React组件需要先编译成原生代码,这个过程可能导致路径映射问题
+3. **类型识别**:插件需要识别标准的 Component 构造器,而不是 React组件
+4. **官方文档示例**:微信官方文档中的所有示例都使用原生格式
+
+## 调试技巧
+
+1. 在微信开发者工具中查看编译后的 `dist/components/novelPlugin/customParagraph/` 目录
+2. 确认以下文件存在:
+   - `index.wxml`
+   - `index.wxss`
+   - `index.js`
+   - `index.json`
+3. 在 `index.js` 的 `attached` 生命周期中添加 console.log 来验证组件是否被加载
+4. 检查微信开发者工具的控制台输出
+
+## 📝 完整使用示例
+
+### 在 app.tsx 中的完整代码
+
+```
+// src/app.tsx
+import Taro from '@tarojs/taro'
+
+// 引入官方小说阅读插件
+const novelPlugin = Taro.requirePlugin('novel-plugin')
+
+class App extends Component {
+  onLaunch(options) {
+    console.log("apponLaunch", options)
+    // 监听进入插件页事件
+    novelPlugin.onPageLoad(this.onNovelPluginLoad)
+  }
+
+  onNovelPluginLoad(data) {
+    console.log('onNovelPluginLoad', data)
+    
+    // data.id - 阅读器实例 id,每个插件页对应一个阅读器实例
+    const novelManager = novelPlugin.getNovelManager(data.id)
+    
+    // 获取自定义参数
+    let customParams = novelManager.getCustomServerParams();
+    if (customParams) {
+      try {
+        customParams = JSON.parse(decodeURIComponent(customParams));
+        console.log("customParams", customParams)
+      } catch (e) {
+        console.error('解析失败', e);
+      }
+    }
+
+    // 设置目录状态(示例)
+    novelManager.setContents({
+      contents: [
+        {
+          index: 0, // 第一章
+          status: 0, // 免费
+        },
+        {
+          index: 1, // 第二章
+          status: 2, // 未解锁
+        },
+        {
+          index: 2, // 第三章
+          status: 1, // 已解锁
+        },
+      ],
+    })
+
+    // ========== 设置自定义段落二维码 ==========
+    
+    // 示例 1:在第一章第 5 段后显示二维码
+    novelManager.setParagraphBlock({
+      chapterIndex: 0,           // 第一章
+      paragraphIndex: 5,         // 第 6 段后
+      ext: JSON.stringify({
+        qrcodeUrl: 'https://corp-msg.oss-cn-hangzhou.aliyuncs.com/17738993366268346745849EF42B39D9459749C6B2784.jpg'
+      })
+    })
+
+    // 示例 2:在第一章第 10 段后显示另一个二维码
+    novelManager.setParagraphBlock({
+      chapterIndex: 0,
+      paragraphIndex: 10,
+      ext: JSON.stringify({
+        qrcodeUrl: 'https://example.com/another-qrcode.jpg',
+        // 可以传递其他自定义数据
+        customData: {
+          campaignId: 'activity-001',
+          source: 'chapter-1'
+        }
+      })
+    })
+
+    // 示例 3:在多个章节都显示二维码
+    for (let chapterIndex = 0; chapterIndex < 10; chapterIndex++) {
+      novelManager.setParagraphBlock({
+        chapterIndex: chapterIndex,
+        paragraphIndex: 8,  // 每章的第 9 段后
+        ext: JSON.stringify({
+          qrcodeUrl: `https://example.com/qrcode?chapter=${chapterIndex}`
+        })
+      })
+    }
+
+    // 示例 4:动态生成带参数的二维码
+    const bookId = customParams?.bookId || ''
+    const qrcodeUrl = `https://your-domain.com/qrcode?bookId=${bookId}&source=novel`
+    
+    novelManager.setParagraphBlock({
+      chapterIndex: 0,
+      paragraphIndex: 3,
+      ext: JSON.stringify({
+        qrcodeUrl: qrcodeUrl
+      })
+    })
+
+    // 监听用户行为事件
+    novelManager.onUserTriggerEvent(res => {
+      console.log('onUserTriggerEvent', res.event_id, res)
+      
+      // 可以在这里处理各种事件
+      switch (res.event_id) {
+        case "start_read":
+          console.log("开始阅读")
+          break
+        case "change_chapter":
+          console.log("切换章节", res)
+          // 可以在这里根据新章节动态设置段落块
+          this.updateDynamicParagraphBlock(novelManager, res.chapter_index)
+          break
+        // ... 其他事件
+      }
+    })
+  }
+
+  // 动态更新段落块的方法
+  updateDynamicParagraphBlock(novelManager, chapterIndex) {
+    // 清除旧的段落块(如果需要)
+    // novelManager.clearParagraphBlocks()
+    
+    // 设置新的段落块
+    novelManager.setParagraphBlock({
+      chapterIndex: chapterIndex,
+      paragraphIndex: 5,
+      ext: JSON.stringify({
+        qrcodeUrl: `https://example.com/dynamic-qrcode?chapter=${chapterIndex}`
+      })
+    })
+  }
+}
+```
+
+### 常见使用场景
+
+#### 场景 1:每章固定位置显示统一二维码
+
+```
+// 在每章的第 10 段后显示相同的二维码
+for (let i = 0; i < totalChapters; i++) {
+  novelManager.setParagraphBlock({
+    chapterIndex: i,
+    paragraphIndex: 10,
+    ext: JSON.stringify({
+      qrcodeUrl: 'https://your-domain.com/fixed-qrcode.jpg'
+    })
+  })
+}
+```
+
+#### 场景 2:根据章节动态显示不同二维码
+
+```
+// 根据章节内容显示不同的推广二维码
+function getQrcodeForChapter(chapterIndex) {
+  if (chapterIndex < 5) {
+    return 'https://example.com/qrcode-early.jpg'
+  } else if (chapterIndex < 20) {
+    return 'https://example.com/qrcode-middle.jpg'
+  } else {
+    return 'https://example.com/qrcode-late.jpg'
+  }
+}
+
+for (let i = 0; i < totalChapters; i++) {
+  novelManager.setParagraphBlock({
+    chapterIndex: i,
+    paragraphIndex: 8,
+    ext: JSON.stringify({
+      qrcodeUrl: getQrcodeForChapter(i)
+    })
+  })
+}
+```
+
+#### 场景 3:只在特定章节显示
+
+```
+// 只在关键章节显示二维码
+const specialChapters = [0, 5, 10, 20] // 第 1、6、11、21 章
+
+specialChapters.forEach(chapterIndex => {
+  novelManager.setParagraphBlock({
+    chapterIndex: chapterIndex,
+    paragraphIndex: 5,
+    ext: JSON.stringify({
+      qrcodeUrl: `https://example.com/special-qrcode-${chapterIndex}.jpg`
+    })
+  })
+})
+```
+
+#### 场景 4:A/B 测试不同的二维码
+
+```
+// 随机显示 A 或 B 版本的二维码
+const useVersionA = Math.random() < 0.5
+
+novelManager.setParagraphBlock({
+  chapterIndex: 0,
+  paragraphIndex: 10,
+  ext: JSON.stringify({
+    qrcodeUrl: useVersionA 
+      ? 'https://example.com/qrcode-a.jpg'
+      : 'https://example.com/qrcode-b.jpg',
+    abTest: useVersionA ? 'A' : 'B'
+  })
+})
+```
+
+## 相关文档
+
+- [微信官方阅读器插件文档](https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/novel.html)
+- [自定义段落](https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/industry/novel.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E6%AE%B5%E8%90%BD)
+- [微信小程序自定义组件](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/)

+ 81 - 0
src/components/novelPlugin/customParagraph/index.js

@@ -0,0 +1,81 @@
+// components/novelPlugin/customParagraph/index.js
+Component({
+  properties: {
+    novelManagerId: {
+      type: Number,
+      value: -1,
+    },
+    bookId: {
+      type: String,
+      value: '',
+    },
+    chapterIndex: {
+      type: Number,
+      value: -1,
+    },
+    chapterId: {
+      type: String,
+      value: '',
+    },
+    paragraphIndex: {
+      type: Number,
+      value: 0,
+    },
+    originalId: {
+      type: String,
+      value: '',
+    },
+    ext: {
+      type: String,
+      value: '',
+    },
+  },
+
+  data: {
+    qrcodeUrl: '', // 二维码图片地址
+  },
+
+  lifetimes: {
+    attached() {
+      console.log('自定义段落组件加载:', {
+        novelManagerId: this.properties.novelManagerId,
+        bookId: this.properties.bookId,
+        chapterIndex: this.properties.chapterIndex,
+        chapterId: this.properties.chapterId,
+        paragraphIndex: this.properties.paragraphIndex,
+        originalId: this.properties.originalId,
+        ext: this.properties.ext,
+      })
+
+      // 解析 ext 参数获取二维码 URL
+      if (this.properties.ext) {
+        try {
+          const extData = JSON.parse(this.properties.ext)
+          console.log('解析 ext 数据:', extData)
+          
+          // 从 ext 中获取二维码 URL
+          if (extData.qrcodeUrl || extData.imageUrl || extData.url) {
+            const qrcodeUrl = extData.qrcodeUrl || extData.imageUrl || extData.url
+            console.log('设置二维码 URL:', qrcodeUrl)
+            this.setData({
+              qrcodeUrl: qrcodeUrl
+            })
+          } else {
+            // 如果没有指定字段,使用默认值
+            console.warn('ext 中没有找到 qrcodeUrl、imageUrl 或 url 字段')
+          }
+        } catch (e) {
+          console.error('解析 ext 失败:', e)
+          // 解析失败时,尝试直接使用 ext 作为 URL
+          if (this.properties.ext.startsWith('http')) {
+            this.setData({
+              qrcodeUrl: this.properties.ext
+            })
+          }
+        }
+      }
+    },
+  },
+
+  methods: {},
+})

+ 4 - 0
src/components/novelPlugin/customParagraph/index.json

@@ -0,0 +1,4 @@
+{
+  "component": true,
+  "usingComponents": {}
+}

+ 12 - 0
src/components/novelPlugin/customParagraph/index.wxml

@@ -0,0 +1,12 @@
+<!--components/novelPlugin/customParagraph/index.wxml-->
+<view class="custom-paragraph-container">
+  <view class="qrcode-box" wx:if="{{qrcodeUrl}}">
+    <image 
+      src="{{qrcodeUrl}}" 
+      class="qrcode-image"
+      mode="aspectFit"
+      show-menu-by-longpress="{{true}}"
+    ></image>
+    <text class="qrcode-tip">长按识别二维码</text>
+  </view>
+</view>

+ 30 - 0
src/components/novelPlugin/customParagraph/index.wxss

@@ -0,0 +1,30 @@
+/* components/novelPlugin/customParagraph/index.wxss */
+.custom-paragraph-container {
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  /* padding: 40px 0; */
+  box-sizing: border-box;
+}
+
+.qrcode-box {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+.qrcode-image {
+  width: 300rpx;
+  height: 300rpx;
+  border-radius: 10rpx;
+  box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
+}
+
+.qrcode-tip {
+  margin-top: 16rpx;
+  font-size: 28rpx;
+  color: #999999;
+  text-align: center;
+}

+ 10 - 0
types/global.d.ts

@@ -18,6 +18,16 @@ declare namespace JSX {
       children?: any;
       styleType?:number;
     }>
+    'custom-paragraph': Partial<{
+      novelManagerId: number;
+      bookId: string;
+      chapterIndex: number;
+      chapterId: string;
+      paragraphIndex: number;
+      originalId: string;
+      ext: string; // 透传参数,JSON 字符串格式
+      style: React.CSSProperties;
+    }>
   }
 }