欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。


React Native Markdown Display

这是一个100% 兼容 CommonMark 的渲染器,一款真正为 React Native 实现的 Markdown 渲染器。
不是基于 WebView 的 Markdown 渲染器,而是使用原生组件渲染所有元素。这些组件可根据需要进行覆写和样式自定义。

在这里插入图片描述


与 react-native-markdown-renderer 的兼容性

本库旨在作为 react-native-markdown-renderer 的替代方案,修复了大量问题并做了功能增强。
由于新版样式规则的工作方式不同,可能需要对你的样式做少量调整,详见下方「如何设置样式」章节。


安装

Yarn

yarn add react-native-markdown-display

NPM

npm install -S react-native-markdown-display

快速开始

import React from 'react';
import { SafeAreaView, ScrollView, StatusBar } from 'react-native';

import Markdown from 'react-native-markdown-display';

const copy = `# 一级标题 8-)

**这是一段粗体文本!**

这是普通文本
`;

const App: () => React$Node = () => {
  return (
    <>
      <StatusBar barStyle="dark-content" />
      <SafeAreaView>
        <ScrollView
          contentInsetAdjustmentBehavior="automatic"
          style={{height: '100%'}}
        >
          <Markdown>
            {copy}
          </Markdown>
        </ScrollView>
      </SafeAreaView>
    </>
  );
};

export default App;

属性与方法

<Markdown> 组件支持以下常用属性:

属性 默认值 必填 说明
children true 要渲染的 Markdown 字符串,或预处理后的 AST 语法树
style 默认样式 false 用于覆盖各类规则的样式对象,详见下方「规则与样式」
mergeStyle true false 若为 true,传入的样式会与默认样式合并,而非完全覆盖
rules 默认渲染规则 false 指定各类 Markdown 元素如何渲染,详见下方「规则」章节
onLinkPress 默认调用 Linking.openURL(url) false 链接点击回调,详见下方「链接处理」章节
debugPrintTree false false 在控制台打印 AST 树,方便查看 Markdown 解析结构

其他较少使用的配置:

属性 默认值 必填 说明
renderer AstRenderer 实例 false 自定义渲染器;使用自定义渲染器后,不能再用 rules 或 styles
markdownit MarkdownIt 实例 false 自定义 markdownit 实例,默认:MarkdownIt({typographer: true})
maxTopLevelChildren null false 若设为数字,只渲染前 N 个顶级子元素,超出后渲染 topLevelMaxExceededItem
topLevelMaxExceededItem <Text key="dotdotdot">...</Text> false 超出最大数量时显示的内容,必须设置 key
allowedImageHandlers ['data:image/png;base64', 'data:image/gif;base64', 'data:image/jpeg;base64', 'https://', 'http://'] false 只允许以此开头的图片地址,其他会拼接 defaultImageHandler
defaultImageHandler http:// false 用于补全不合法的图片 URL;设为 null 则直接不渲染该图片

语法支持

标题

# 一级标题
## 二级标题
### 三级标题
#### 四级标题
##### 五级标题
###### 六级标题
分割线

上方文本
___

中间文本

---

下方文本
强调

**粗体文本**
__粗体文本__

*斜体文本*
_斜体文本_

~~删除线~~
引用块

> 引用可以嵌套…
>> …连续使用大于号即可…
> > > …也可以在符号间加空格。
列表

无序列表

+ 以 +、-、* 开头创建列表
+ 缩进 2 个空格即可创建子列表:
  - 符号不同会强制新开一组列表:
    * 列表项文本
    + 很长的列表项会自动换行
    - 另一个列表项
+ 使用非常简单!

有序列表

1. 第一项
2. 第二项,很长文本自动换行
3. 第三项

指定起始序号:

57. foo
58. bar
iOS Android
代码

行内 `代码`

缩进式代码

    // 注释
    代码行 1
    代码行 2
    代码行 3

代码块围栏

示例文本…


语法高亮

```js
var foo = function (bar) {
  return bar++;
};

console.log(foo(5));
表格

| 选项 | 说明 |
| ---- | ----------- |
| data | 数据文件路径 |
| engine | 模板引擎 |
| ext | 目标文件扩展名 |

右对齐列

| 选项 | 说明 |
| -----:| -----------:|
| data | 数据文件路径 |
| engine | 模板引擎 |
| ext | 目标文件扩展名 |
iOS Android
链接

[链接文字](https://www.google.com)

[带标题的链接](https://www.google.com "标题")

自动识别链接 https://www.google.com(需开启 linkify)
iOS Android
图片

![Minion](https://octodex.github.com/images/minion.png)
![Stormtroopocat](https://octodex.github.com/images/stormtroopocat.jpg "标题")

也支持脚注式图片语法

![替代文字][id]

在文档下方定义 URL:

[id]: https://octodex.github.com/images/dojocat.jpg  "标题"
iOS Android
排版替换

开启 typographer 后可见效果。

(c) (C) (r) (R) (tm) (TM) (p) (P) +-

test.. test... test..... test?..... test!....

!!!!!! ???? ,,  -- ---

"智能引号" 和 '单引号'
插件与扩展

可通过任意 markdown-it 兼容插件扩展语法,插件列表见:markdown-it 插件
以下是集成示例:

第一步

找到新元素并集成插件,可使用 debugPrintTree 查看渲染规则:

import React from 'react';
import { SafeAreaView, ScrollView, StatusBar } from 'react-native';

import Markdown, { MarkdownIt } from 'react-native-markdown-display';
import blockEmbedPlugin from 'markdown-it-block-embed';

const markdownItInstance = 
    MarkdownIt({typographer: true})
      .use(blockEmbedPlugin, {
        containerClassName: "video-embed"
      });

const copy = `
# 标题

@[youtube](lJIrF4YjHfQ)
`;

const App: () => React$Node = () => {
  return (
    <>
      <StatusBar barStyle="dark-content" />
      <SafeAreaView>
        <ScrollView
          contentInsetAdjustmentBehavior="automatic"
          style={{height: '100%'}}
        >
            <Markdown
              debugPrintTree
              markdownit={markdownItInstance}
            >
              {copy}
            </Markdown>
        </ScrollView>
      </SafeAreaView>
    </>
  );
};

export default App;

控制台会输出:

body
-heading1
--textgroup
---text
-video

并提示:

Warning, unknown render rule encountered: video. 'unknown' render rule used (by default, returns null - nothing rendered) 

第二步

为新的 video 组件创建渲染规则样式

import React from 'react';
import { SafeAreaView, ScrollView, StatusBar } from 'react-native';

import Markdown, { MarkdownIt } from 'react-native-markdown-display';
import blockEmbedPlugin from 'markdown-it-block-embed';

const markdownItInstance = 
    MarkdownIt({typographer: true})
      .use(blockEmbedPlugin, {
        containerClassName: "video-embed"
      });

const copy = `
# 标题

@[youtube](lJIrF4YjHfQ)
`;

const App: () => React$Node = () => {
  return (
    <>
      <StatusBar barStyle="dark-content" />
      <SafeAreaView>
        <ScrollView
          contentInsetAdjustmentBehavior="automatic"
          style={{height: '100%'}}
        >
            <Markdown
              debugPrintTree
              markdownit={markdownItInstance}
              style={{
                  video: {
                    color: 'red',
                  }
                }}
              rules={{
                  video: (node, children, parent, styles) =>{
                    // 查看 node 信息判断要渲染哪种视频
                    console.log(node);

                    return (<Text key={node.key} style={styles.video}>
                      这里返回视频组件!
                    </Text>);
                  }
                   
                }}
            >
              {copy}
            </Markdown>
        </ScrollView>
      </SafeAreaView>
    </>
  );
};

export default App;

视频相关信息会挂在 node 上:

{type: "video", sourceType: "video", sourceInfo: {…}, sourceMeta: null, block: true, …}
  attributes: {}
  block: true
  children: []
  content: ""
  index: 1
  key: "rnmr_1720a98f540_video"
  markup: "@[youtube](lJIrF4YjHfQ)"
  sourceInfo:
    service: YouTubeService
    videoID: "lJIrF4YjHfQ"
    ...

其他调试方式

import Markdown, { MarkdownIt } from 'react-native-markdown-display';
import blockEmbedPlugin from 'markdown-it-block-embed';

const markdownItInstance = 
    MarkdownIt({typographer: true})
      .use(blockEmbedPlugin, {
        containerClassName: "video-embed"
      });

const copy = `
# 标题

@[youtube](lJIrF4YjHfQ)
`;

// 查看本库使用的 AST 树
const astTree = markdownItInstance.parse(copy, {});
console.log(astTree);

// 查看生成的 HTML(本库不使用,仅作参考)
const html = markdownItInstance.render(copy);
console.log(html);
完整测试 Markdown

用于一次性测试所有样式与渲染:

标题

# 一级标题
## 二级标题
### 三级标题
#### 四级标题
##### 五级标题
###### 六级标题

分割线

上方文本
___

中间文本

---

下方文本

强调

**粗体**
__粗体__
*斜体*
_斜体_
~~删除线~~

引用

> 可嵌套
>> 多层
> > > 带空格

列表

无序列表
+ 项目
+ 子项目
  - 子项

有序列表
1. 第一项
2. 第二项

代码

行内 `code`

代码块


表格

| 选项 | 说明 |
| ---- | ----------- |
| a | b |

链接

[文字](url)

图片

![图片](url)

规则与样式

如何设置样式

文本样式设计类似 CSS:全局样式作用于所有内容,下层可覆盖上层。

注意 text 样式:并非应用到所有文本,比如列表圆点就不受影响。
想统一修改所有文字颜色,应修改 body 样式。

示例

import React from 'react';
import { SafeAreaView, ScrollView, StatusBar } from 'react-native';

import Markdown from 'react-native-markdown-display';

const copy = `
这段文本会是红色小号字体,因为继承自 body 样式。

// 代码块
const cool = () => {};


更多小号文本

# 一级标题
## 二级标题
### 三级标题
`;

const App: () => React$Node = () => {
  return (
    <>
      <StatusBar barStyle="dark-content" />
      <SafeAreaView>
        <ScrollView
          contentInsetAdjustmentBehavior="automatic"
          style={{height: '100%'}}
        >
            <Markdown
              style={{
                body: {color: 'red', fontSize: 10},
                heading1: {color: 'purple'},
                code_block: {color: 'black', fontSize: 14}
              }}
            >
              {copy}
            </Markdown>
        </ScrollView>
      </SafeAreaView>
    </>
  );
};

export default App;

样式

样式用于覆盖各规则的默认样式,默认样式见:
默认样式

注意:默认会与默认样式合并,可通过 mergeStyle 关闭。

样式示例

import React from 'react';
import { SafeAreaView, ScrollView, StatusBar, StyleSheet } from 'react-native';

import Markdown from 'react-native-markdown-display';

const styles = StyleSheet.create({
  heading1: {
    fontSize: 32,
    backgroundColor: '#000',
    color: '#fff',
  },
  heading2: {
    fontSize: 24,
  },
});

const copy = `
# 一级标题
## 二级标题
`;

const App: () => React$Node = () => {
  return (
    <>
      <StatusBar barStyle="dark-content" />
      <SafeAreaView>
        <ScrollView
          contentInsetAdjustmentBehavior="automatic"
          style={{height: '100%'}}
        >
            <Markdown style={styles}>
              {copy}
            </Markdown>
        </ScrollView>
      </SafeAreaView>
    </>
  );
};

export default App;

规则

规则用于自定义元素渲染方式,默认规则见:
默认规则

规则示例

import React from 'react';
import { SafeAreaView, ScrollView, StatusBar, Text } from 'react-native';

import Markdown from 'react-native-markdown-display';

const rules = {
    heading1: (node, children, parent, styles) =>
      <Text key={node.key} style={[styles.heading, styles.heading1]}>
        >> H1:"{children}"
      </Text>,
    heading2: (node, children, parent, styles) =>
      <Text key={node.key} style={[styles.heading, styles.heading2]}>
        >> H2:"{children}"
      </Text>,
};

const copy = `
# 一级标题
## 二级标题
`;

const App: () => React$Node = () => {
  return (
    <>
      <StatusBar barStyle="dark-content" />
      <SafeAreaView>
        <ScrollView
          contentInsetAdjustmentBehavior="automatic"
          style={{height: '100%'}}
        >
            <Markdown rules={rules}>
              {copy}
            </Markdown>
        </ScrollView>
      </SafeAreaView>
    </>
  );
};

export default App;

所有规则及对应样式

渲染规则 样式名
body body
heading1heading6 heading1heading6
hr hr
strong strong
em em
s s
blockquote blockquote
bullet_list bullet_list
ordered_list ordered_list
list_item list_item(含伪类:ordered_list_iconbullet_list_icon 等)
code_inline code_inline
code_block code_block
fence fence
table/thead/tbody/th/tr/td 同名样式
link link
blocklink blocklink
image image
text text
paragraph paragraph

链接处理

默认行为:使用 React Native Linking.openURL(url) 打开链接。

可通过两种方式自定义:

onLinkPress 回调

import React from 'react';
import { SafeAreaView, ScrollView, StatusBar } from 'react-native';

import Markdown from 'react-native-markdown-display';

const copy = `[这是一个链接!](https://github.com/iamacup/react-native-markdown-display/)`;

const onLinkPress = (url) => {
    if (url) {
      // 自定义逻辑
      return false;
    }
    // 返回 true 则继续用默认打开方式
    return true
  }

const App: () => React$Node = () => {
  return (
    <>
      <StatusBar barStyle="dark-content" />
      <SafeAreaView>
        <ScrollView
          contentInsetAdjustmentBehavior="automatic"
          style={{height: '100%'}}
        >
          <Markdown onLinkPress={onLinkPress}>
            {copy}
          </Markdown>
        </ScrollView>
      </SafeAreaView>
    </>
  );
};

export default App;
自定义 link/blocklink 规则

import React from 'react';
import { SafeAreaView, ScrollView, StatusBar, Text } from 'react-native';

import Markdown from 'react-native-markdown-display';

const copy = `[链接](url)`;

const rules = {
  link: (node, children, parent, styles) => {
    return (
      <Text
        key={node.key}
        style={styles.link}
        onPress={() => 自定义处理(node.attributes.href)}
      >
        {children}
      </Text>
    );
  },
};

const App: () => React$Node = () => {
  return (
    <>
      <StatusBar barStyle="dark-content" />
      <SafeAreaView>
        <ScrollView style={{height: '100%'}}>
          <Markdown rules={rules}>
            {copy}
          </Markdown>
        </ScrollView>
      </SafeAreaView>
    </>
  );
};

export default App;

禁用某些 Markdown 语法

可通过 markdownit.disable([...]) 禁用不需要的语法,适合移动端精简场景。

示例:禁用图片和链接:

import React from 'react';
import { SafeAreaView, ScrollView, StatusBar } from 'react-native';

import Markdown, { MarkdownIt } from 'react-native-markdown-display';

const copy = `
# 标题正常显示

[但链接只会显示为普通文本](不会跳转)
`;

const App: () => React$Node = () => {
  return (
    <>
      <StatusBar barStyle="dark-content" />
      <SafeAreaView>
        <ScrollView style={{height: '100%'}}>
            <Markdown
              markdownit={
                MarkdownIt({typographer: true}).disable([ 'link', 'image' ])
              }
            >
              {copy}
            </Markdown>
        </ScrollView>
      </SafeAreaView>
    </>
  );
};

export default App;

可禁用的完整列表见:
https://github.com/markdown-it/markdown-it/blob/master/lib/presets/commonmark.js


预处理

如需在外部预处理 Markdown,可直接传入 AST 树:

import React from 'react';
import { SafeAreaView, ScrollView, StatusBar } from 'react-native';

import Markdown, { MarkdownIt, tokensToAST, stringToTokens } from 'react-native-markdown-display';

const markdownItInstance = MarkdownIt({typographer: true});

const copy = `
# 标题

带**粗体**的文本
`;

const ast = tokensToAST(stringToTokens(copy, markdownItInstance))

const App: () => React$Node = () => {
  return (
    <>
      <StatusBar barStyle="dark-content" />
      <SafeAreaView>
        <ScrollView style={{height: '100%'}}>
            <Markdown>
              {ast}
            </Markdown>
        </ScrollView>
      </SafeAreaView>
    </>
  );
};

export default App;

完整代码:

import React, { useState } from 'react';
import {
  StyleSheet,
  ScrollView,
  View,
  Text,
  SafeAreaView,
  TouchableOpacity,
  Image,
  Dimensions,
} from 'react-native';
import Markdown from 'react-native-markdown-display';

// Base64 Icons
const PRODUCT_ICONS = {
  back: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE1IDE1TDEwIDEyIDE1IDkiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+Cg==',
  share: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE4IDdDNy4yIDcgNy4yIDIxIDE4IDIxIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8cGF0aCBkPSJNMTggN0M3LjIgNyA3LjIgMjEgMTggMjEiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik0xOCA3QzcuMiA3IDcuMiAyMSAxOCAyMSIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPC9zdmc+Cg==',
  favorite: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDIxLjVMMSAxMS4yQzEuNjIgOS41MiAzLjQgOC4yNSA1LjQyIDcuNzFDNy40NSA3LjE3IDkuNjMgNy40IDExLjQ1IDguM0wxMiA4LjU4TDEyLjU1IDguM0MxNC4zNyA3LjQgMTYuNTUgNy4xNyAxOC41OCA3LjcxdDIuODEgMi40M0wxMjIuNSAyMS41WiIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPC9zdmc+Cg==',
  cart: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTcgMkg1Yy0xLjEgMC0xLjk5LjktMS45OSAyTDUgMjFINyIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPHBhdGggZD0iTTE5IDIwSDhjLTEuMSAwLTIgLjktMiAydjFjMCAuMS45IDEgMiAxaDExYzEuMSAwIDItLjkgMi0ydi0xYzAtMS4xLS45LTItMi0yeiIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPHBhdGggZD0iTTE3IDE2YTEgMSAwIDAgMCAxLTF2LTE0YTItMiAwIDAgMC0yLTJoLTRhMiAyIDAgMCAwLTIgMnYxNGMwIC41LjQ0IDEgMSAxaDJ6IiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K',
  star: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggIGQ9Ik0xMiAyLjVMMTQuMDkgNy41MkwxOS41IDEwLjQ4TDE1LjkxIDEzLjQ0TDE3LjE4IDE5LjA5TDEyIDE2LjIzTDYuODIgMTkuMDlMOC4wOSAxMy40NEwyLjY4IDEwLjQ4TDguMDkgNy41MkwxMiAyLjVaIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K',
  check: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTkgMTdsNC00LTQtNCIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L3N2Zz4K'
};

const ProductDetailScreen = () => {
  const [isFavorite, setIsFavorite] = useState(false);
  const [selectedImage, setSelectedImage] = useState(0);

  // 商品详细信息 Markdown
  const productDetailMarkdown = `
# 📱 iPhone 15 Pro Max

## 🔥 产品亮点

### 🎨 设计工艺
- **钛金属机身** - 更轻更坚固,质感出众
- **全新操作按钮** - 可自定义功能,操作更便捷
- **多彩配色** - 自然钛金属、蓝色钛金属、白色钛金属、黑色钛金属

### 📸 摄影系统
- **4800万像素主摄** - 全新传感器,画质更出色
- **5倍光学变焦** - 远摄能力大幅提升
- **电影模式** - 专业级视频拍摄体验
- **夜间模式** - 暗光环境下也能拍出好照片

### ⚡ 性能配置
- **A17 Pro芯片** - 业界首款3纳米制程
- **8GB运行内存** - 多任务处理更流畅
- **最高1TB存储** - 海量空间满足需求
- **USB-C接口** - 传输速度更快,兼容性更好

### 🔋 续航表现
- **视频播放最长29小时**
- **支持20W有线快充**
- **支持15W MagSafe无线充电**
- **支持7.5W Qi无线充电**

## 📦 包装清单

✅ iPhone 15 Pro Max  
✅ USB-C 转 Lightning 数据线  
✅ 文档资料  

## 💡 使用技巧

### 拍照技巧
- 使用**人像模式**获得专业虚化效果
- 开启**HDR**功能提升画面动态范围
- 利用**实况照片**捕捉精彩瞬间

### 日常使用
- 设置**专注模式**提高工作效率
- 启用**低电量模式**延长续航时间
- 使用**快捷指令**自动化日常操作

## 🛡️ 售后保障

- **一年有限保修**
- **AppleCare+服务**(额外付费)
- **全球联保服务**
- **技术支持热线**

---

*价格:¥9,999起*  
*库存:充足*
`;

  const productImages = [
    'https://images.unsplash.com/photo-1607503242738-4e5c709c6a07?w=400&h=400&fit=crop',
    'https://images.unsplash.com/photo-1607504932500-bf4d8c09d6c9?w=400&h=400&fit=crop',
    'https://images.unsplash.com/photo-1607502545814-d032a5c0f8cc?w=400&h=400&fit=crop',
    'https://images.unsplash.com/photo-1607501499296-4aff2b67f8be?w=400&h=400&fit=crop'
  ];

  const handleBack = () => {
    console.log('返回上一页');
  };

  const handleShare = () => {
    console.log('分享商品');
  };

  const handleFavorite = () => {
    setIsFavorite(!isFavorite);
    console.log('收藏状态:', !isFavorite);
  };

  const handleAddToCart = () => {
    console.log('加入购物车');
  };

  const handleBuyNow = () => {
    console.log('立即购买');
  };

  return (
    <SafeAreaView style={styles.container}>
      {/* 顶部导航栏 */}
      <View style={styles.header}>
        <TouchableOpacity style={styles.headerButton} onPress={handleBack}>
          <Image source={{ uri: PRODUCT_ICONS.back }} style={styles.headerIcon} />
        </TouchableOpacity>
        <Text style={styles.headerTitle}>商品详情</Text>
        <View style={styles.headerActions}>
          <TouchableOpacity style={styles.headerButton} onPress={handleShare}>
            <Image source={{ uri: PRODUCT_ICONS.share }} style={styles.headerIcon} />
          </TouchableOpacity>
          <TouchableOpacity style={styles.headerButton} onPress={handleFavorite}>
            <Image 
              source={{ uri: PRODUCT_ICONS.favorite }} 
              style={[styles.headerIcon, isFavorite && styles.favoritedIcon]} 
            />
          </TouchableOpacity>
        </View>
      </View>

      <ScrollView style={styles.scrollView}>
        {/* 商品图片轮播 */}
        <View style={styles.imageSection}>
          <Image 
            source={{ uri: productImages[selectedImage] }} 
            style={styles.mainImage} 
          />
          <View style={styles.imageDots}>
            {productImages.map((_, index) => (
              <TouchableOpacity
                key={index}
                style={[
                  styles.dot,
                  selectedImage === index && styles.activeDot
                ]}
                onPress={() => setSelectedImage(index)}
              />
            ))}
          </View>
        </View>

        {/* 商品基本信息 */}
        <View style={styles.infoSection}>
          <Text style={styles.productTitle}>iPhone 15 Pro Max</Text>
          <View style={styles.priceContainer}>
            <Text style={styles.currentPrice}>¥9,999</Text>
            <Text style={styles.originalPrice}>¥10,999</Text>
          </View>
          <View style={styles.ratingContainer}>
            <View style={styles.stars}>
              {[1, 2, 3, 4, 5].map((star) => (
                <Image 
                  key={star}
                  source={{ uri: PRODUCT_ICONS.star }} 
                  style={styles.starIcon} 
                />
              ))}
            </View>
            <Text style={styles.ratingText}>4.9 (1.2+评价)</Text>
          </View>
        </View>

        {/* 促销信息 */}
        <View style={styles.promotionSection}>
          <View style={styles.promotionTag}>
            <Text style={styles.promotionText}>限时优惠</Text>
          </View>
          <Text style={styles.promotionDesc}>购买即享价值¥299的保护壳免费赠送</Text>
        </View>

        {/* 商品详情 Markdown 渲染 */}
        <View style={styles.detailSection}>
          <View style={styles.sectionHeader}>
            <Text style={styles.sectionTitle}>商品详情</Text>
            <View style={styles.divider} />
          </View>
          <View style={styles.markdownContainer}>
            <Markdown style={markdownStyles}>
              {productDetailMarkdown}
            </Markdown>
          </View>
        </View>

        {/* 规格参数 */}
        <View style={styles.specSection}>
          <View style={styles.sectionHeader}>
            <Text style={styles.sectionTitle}>规格参数</Text>
            <View style={styles.divider} />
          </View>
          <View style={styles.specList}>
            <View style={styles.specItem}>
              <Text style={styles.specKey}>品牌</Text>
              <Text style={styles.specValue}>Apple</Text>
            </View>
            <View style={styles.specItem}>
              <Text style={styles.specKey}>型号</Text>
              <Text style={styles.specValue}>iPhone 15 Pro Max</Text>
            </View>
            <View style={styles.specItem}>
              <Text style={styles.specKey}>存储容量</Text>
              <Text style={styles.specValue}>256GB</Text>
            </View>
            <View style={styles.specItem}>
              <Text style={styles.specKey}>颜色</Text>
              <Text style={styles.specValue}>自然钛金属</Text>
            </View>
            <View style={styles.specItem}>
              <Text style={styles.specKey}>屏幕尺寸</Text>
              <Text style={styles.specValue}>6.7英寸</Text>
            </View>
          </View>
        </View>
      </ScrollView>

      {/* 底部操作栏 */}
      <View style={styles.bottomBar}>
        <TouchableOpacity style={styles.cartButton} onPress={handleAddToCart}>
          <Image source={{ uri: PRODUCT_ICONS.cart }} style={styles.barIcon} />
          <Text style={styles.barButtonText}>加入购物车</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.buyButton} onPress={handleBuyNow}>
          <Text style={styles.buyButtonText}>立即购买</Text>
        </TouchableOpacity>
      </View>
    </SafeAreaView>
  );
};

// Markdown 样式配置
const markdownStyles = {
  heading1: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#1a1a1a',
    marginBottom: 12,
    marginTop: 16,
  },
  heading2: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#1a1a1a',
    marginBottom: 10,
    marginTop: 14,
  },
  heading3: {
    fontSize: 18,
    fontWeight: '600',
    color: '#1a1a1a',
    marginBottom: 8,
    marginTop: 12,
  },
  text: {
    fontSize: 16,
    color: '#333333',
    lineHeight: 24,
    marginBottom: 8,
  },
  strong: {
    fontWeight: 'bold',
    color: '#1a1a1a',
  },
  em: {
    fontStyle: 'italic',
  },
  bullet_list: {
    paddingLeft: 20,
    marginBottom: 12,
  },
  ordered_list: {
    paddingLeft: 20,
    marginBottom: 12,
  },
  listItem: {
    fontSize: 16,
    color: '#333333',
    lineHeight: 24,
    marginBottom: 4,
  },
  code_inline: {
    backgroundColor: '#f0f0f0',
    paddingHorizontal: 4,
    paddingVertical: 2,
    borderRadius: 4,
    fontFamily: 'monospace',
  },
  fence: {
    backgroundColor: '#f8f8f8',
    padding: 12,
    borderRadius: 8,
    marginBottom: 12,
    borderWidth: 1,
    borderColor: '#e0e0e0',
  },
  blockquote: {
    borderLeftWidth: 4,
    borderLeftColor: '#2196F3',
    paddingLeft: 16,
    marginLeft: 8,
    backgroundColor: '#f5f9ff',
    paddingVertical: 8,
    marginBottom: 12,
  },
  hr: {
    height: 1,
    backgroundColor: '#e0e0e0',
    marginVertical: 16,
  },
};

const { width, height } = Dimensions.get('window');

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5',
  },
  header: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    backgroundColor: '#ffffff',
    paddingHorizontal: 16,
    paddingVertical: 12,
    borderBottomWidth: 1,
    borderBottomColor: '#e0e0e0',
    elevation: 2,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
  },
  headerButton: {
    padding: 8,
    borderRadius: 20,
  },
  headerIcon: {
    width: 24,
    height: 24,
    tintColor: '#333333',
  },
  favoritedIcon: {
    tintColor: '#FF4444',
  },
  headerTitle: {
    fontSize: 18,
    fontWeight: '600',
    color: '#1a1a1a',
  },
  headerActions: {
    flexDirection: 'row',
    alignItems: 'center',
  },
  scrollView: {
    flex: 1,
  },
  imageSection: {
    backgroundColor: '#ffffff',
    padding: 16,
  },
  mainImage: {
    width: width - 32,
    height: width - 32,
    borderRadius: 12,
    resizeMode: 'cover',
  },
  imageDots: {
    flexDirection: 'row',
    justifyContent: 'center',
    marginTop: 12,
  },
  dot: {
    width: 8,
    height: 8,
    borderRadius: 4,
    backgroundColor: '#cccccc',
    marginHorizontal: 4,
  },
  activeDot: {
    backgroundColor: '#2196F3',
    width: 20,
  },
  infoSection: {
    backgroundColor: '#ffffff',
    padding: 20,
    marginTop: 8,
  },
  productTitle: {
    fontSize: 22,
    fontWeight: 'bold',
    color: '#1a1a1a',
    marginBottom: 12,
  },
  priceContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 12,
  },
  currentPrice: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#FF4444',
    marginRight: 8,
  },
  originalPrice: {
    fontSize: 16,
    color: '#999999',
    textDecorationLine: 'line-through',
  },
  ratingContainer: {
    flexDirection: 'row',
    alignItems: 'center',
  },
  stars: {
    flexDirection: 'row',
    marginRight: 8,
  },
  starIcon: {
    width: 16,
    height: 16,
    tintColor: '#FFD700',
    marginRight: 2,
  },
  ratingText: {
    fontSize: 14,
    color: '#666666',
  },
  promotionSection: {
    backgroundColor: '#FFF8E1',
    padding: 16,
    margin: 16,
    borderRadius: 12,
    borderLeftWidth: 4,
    borderLeftColor: '#FFC107',
  },
  promotionTag: {
    backgroundColor: '#FFC107',
    paddingHorizontal: 12,
    paddingVertical: 4,
    borderRadius: 12,
    alignSelf: 'flex-start',
    marginBottom: 8,
  },
  promotionText: {
    fontSize: 12,
    fontWeight: '600',
    color: '#333333',
  },
  promotionDesc: {
    fontSize: 14,
    color: '#555555',
    lineHeight: 20,
  },
  detailSection: {
    backgroundColor: '#ffffff',
    margin: 16,
    borderRadius: 12,
    padding: 20,
    elevation: 2,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
  },
  specSection: {
    backgroundColor: '#ffffff',
    margin: 16,
    borderRadius: 12,
    padding: 20,
    elevation: 2,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
    marginBottom: 80,
  },
  sectionHeader: {
    marginBottom: 16,
  },
  sectionTitle: {
    fontSize: 18,
    fontWeight: '600',
    color: '#1a1a1a',
    marginBottom: 12,
  },
  divider: {
    height: 1,
    backgroundColor: '#e0e0e0',
  },
  markdownContainer: {
    backgroundColor: '#fafafa',
    borderRadius: 8,
    padding: 16,
  },
  specList: {
    backgroundColor: '#fafafa',
    borderRadius: 8,
    padding: 16,
  },
  specItem: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    paddingVertical: 12,
    borderBottomWidth: 1,
    borderBottomColor: '#eeeeee',
  },
  specKey: {
    fontSize: 16,
    color: '#666666',
    fontWeight: '500',
  },
  specValue: {
    fontSize: 16,
    color: '#333333',
    fontWeight: '600',
  },
  bottomBar: {
    flexDirection: 'row',
    backgroundColor: '#ffffff',
    paddingHorizontal: 16,
    paddingVertical: 12,
    borderTopWidth: 1,
    borderTopColor: '#e0e0e0',
    elevation: 10,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: -2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
  },
  cartButton: {
    flex: 1,
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#f0f0f0',
    borderRadius: 25,
    paddingVertical: 14,
    marginRight: 12,
  },
  buyButton: {
    flex: 2,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#2196F3',
    borderRadius: 25,
    paddingVertical: 14,
  },
  barIcon: {
    width: 20,
    height: 20,
    tintColor: '#333333',
    marginRight: 8,
  },
  barButtonText: {
    fontSize: 16,
    fontWeight: '500',
    color: '#333333',
  },
  buyButtonText: {
    fontSize: 16,
    fontWeight: '600',
    color: '#ffffff',
  },
});

export default ProductDetailScreen;

在这里插入图片描述


接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

在这里插入图片描述
打包之后再将打包后的鸿蒙OpenHarmony文件拷贝到鸿蒙的DevEco-Studio工程目录去:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
React Native Markdown Display是一款完全兼容CommonMark标准的Markdown渲染器,专为React Native设计。它使用原生组件而非WebView来渲染所有元素,支持自定义样式和组件覆写。该库修复了react-native-markdown-renderer的诸多问题,并增强了功能。安装简便,支持Yarn和NPM两种方式。组件提供丰富的配置选项,包括样式覆盖、链接处理、调试打印等。支持所有常见Markdown语法,如标题、列表、代码块、表格等,并配有详细的示例说明。开发者可以轻松集成到项目中,构建美观的Markdown展示界面。

欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐