Merge remote-tracking branch 'origin/master'

main
king 1 year ago
commit be80cbab2d
  1. 1
      package.json
  2. 29
      pnpm-lock.yaml
  3. 4
      src/api/brand.ts
  4. 1
      src/app.config.ts
  5. 4
      src/pages/preview/brand/article/article.tsx
  6. 10
      src/pages/preview/brand/info/info.module.scss
  7. 131
      src/pages/preview/brand/info/info.tsx
  8. 25
      src/pages/preview/illness/article/article.module.scss
  9. 52
      src/pages/preview/illness/article/article.tsx
  10. 11
      src/pages/preview/illness/list/list.tsx
  11. 4
      src/pages/preview/webView/webView.config.ts
  12. 9
      src/pages/preview/webView/webView.tsx
  13. 30
      src/utils/marked/components/Tablink.tsx
  14. 174
      src/utils/marked/marked.scss
  15. 319
      src/utils/marked/marked.ts
  16. 86
      src/utils/marked/utils.ts

@ -54,6 +54,7 @@
"@tarojs/shared": "3.6.8", "@tarojs/shared": "3.6.8",
"@tarojs/taro": "3.6.8", "@tarojs/taro": "3.6.8",
"dayjs": "^1.11.9", "dayjs": "^1.11.9",
"marked": "^7.0.4",
"react": "^18.0.0", "react": "^18.0.0",
"react-dom": "^18.0.0", "react-dom": "^18.0.0",
"react-refresh": "^0.11.0", "react-refresh": "^0.11.0",

@ -53,6 +53,9 @@ dependencies:
dayjs: dayjs:
specifier: ^1.11.9 specifier: ^1.11.9
version: 1.11.9 version: 1.11.9
marked:
specifier: ^7.0.4
version: 7.0.4
react: react:
specifier: ^18.0.0 specifier: ^18.0.0
version: 18.0.0 version: 18.0.0
@ -4993,6 +4996,13 @@ packages:
- supports-color - supports-color
dev: true dev: true
/babel-runtime@6.26.0:
resolution: {integrity: sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==}
dependencies:
core-js: 2.6.12
regenerator-runtime: 0.11.1
dev: false
/balanced-match@1.0.2: /balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
dev: true dev: true
@ -5831,6 +5841,12 @@ packages:
resolution: {integrity: sha512-/AnE9Y4OsJZicCzIe97JP5XoPKQJfTuEG43aEVLFJGOJpyqELod+pE6LEl63DfG1Mp8wX97LDaDpy1GmLEUxlg==} resolution: {integrity: sha512-/AnE9Y4OsJZicCzIe97JP5XoPKQJfTuEG43aEVLFJGOJpyqELod+pE6LEl63DfG1Mp8wX97LDaDpy1GmLEUxlg==}
requiresBuild: true requiresBuild: true
/core-js@2.6.12:
resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==}
deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.
requiresBuild: true
dev: false
/core-js@3.31.0: /core-js@3.31.0:
resolution: {integrity: sha512-NIp2TQSGfR6ba5aalZD+ZQ1fSxGhDo/s1w0nx3RYzf2pnJxt7YynxFlFScP6eV7+GZsKO95NSjGxyJsU3DZgeQ==} resolution: {integrity: sha512-NIp2TQSGfR6ba5aalZD+ZQ1fSxGhDo/s1w0nx3RYzf2pnJxt7YynxFlFScP6eV7+GZsKO95NSjGxyJsU3DZgeQ==}
requiresBuild: true requiresBuild: true
@ -9291,6 +9307,12 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/marked@7.0.4:
resolution: {integrity: sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ==}
engines: {node: '>= 16'}
hasBin: true
dev: false
/mathml-tag-names@2.1.3: /mathml-tag-names@2.1.3:
resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==}
dev: true dev: true
@ -11037,7 +11059,6 @@ packages:
/regenerator-runtime@0.11.1: /regenerator-runtime@0.11.1:
resolution: {integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==} resolution: {integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==}
dev: true
/regenerator-runtime@0.13.11: /regenerator-runtime@0.13.11:
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
@ -12093,6 +12114,12 @@ packages:
postcss-value-parser: 3.3.1 postcss-value-parser: 3.3.1
dev: true dev: true
/taro-parse@1.1.5:
resolution: {integrity: sha512-OS8O9gPyFNQCvQuaGPMshNtxpmvSGVNTNkT/YPhozs629/f+PDEIWt1Ahlqu5uOik58x4piiaogiUdon218Rfg==}
dependencies:
babel-runtime: 6.26.0
dev: false
/terser-webpack-plugin@5.3.9(@swc/core@1.3.23)(esbuild@0.14.54)(webpack@5.78.0): /terser-webpack-plugin@5.3.9(@swc/core@1.3.23)(esbuild@0.14.54)(webpack@5.78.0):
resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==} resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==}
engines: {node: '>= 10.13.0'} engines: {node: '>= 10.13.0'}

@ -30,11 +30,11 @@ export const brandApi = {
return request<BrandRecord>(`/home/v1/brand/${id}`, "GET") return request<BrandRecord>(`/home/v1/brand/${id}`, "GET")
}, },
/** 文章列表 */ /** 文章列表 */
articleList(owner_id: number ) { articleList(owner_id: number,page:number) {
return request<{ return request<{
list: ArticleRecord[], list: ArticleRecord[],
total: number total: number
}>(`/home/v1/article/list?owner_id=${owner_id}&page=1&page_size=1000` , "GET") }>(`/home/v1/article/list?owner_id=${owner_id}&page=${page}&page_size=10` , "GET")
}, },
articleInfo(id: number ) { articleInfo(id: number ) {
return request<ArticleRecord>(`/home/v1/article/${id}` , "GET") return request<ArticleRecord>(`/home/v1/article/${id}` , "GET")

@ -89,6 +89,7 @@ export default defineAppConfig({
'illness/sort/sort', 'illness/sort/sort',
'illness/list/list', 'illness/list/list',
'illness/article/article', 'illness/article/article',
'webView/webView',
] ]
}, },
], ],

@ -8,7 +8,7 @@ import {Profile} from "@/store";
const article:FC = () => { const article:FC = () => {
const {token} = Profile.useContainer() const {token,empty} = Profile.useContainer()
const {id} = useRouter().params as unknown as { id: number} const {id} = useRouter().params as unknown as { id: number}
const [articleInfo,setArticleInfo] = useState<ArticleRecord>() const [articleInfo,setArticleInfo] = useState<ArticleRecord>()
useEffect(() => { useEffect(() => {
@ -35,7 +35,7 @@ const article:FC = () => {
<View className={styles['fixedBox-inner-icon']}> <View className={styles['fixedBox-inner-icon']}>
<Image src={down}></Image> <Image src={down}></Image>
</View> </View>
<View className={styles['fixedBox-inner-box']}> <View className={styles['fixedBox-inner-box']} onClick={empty}>
<Text></Text> <Text></Text>
</View> </View>
</View> </View>

@ -7,7 +7,7 @@ page{
} }
.curIndexBox{ .curIndexBox{
position: absolute; position: absolute;
top:450rpx; top:500rpx;
right:30rpx; right:30rpx;
background-color: rgba(0,0,0,0.5); background-color: rgba(0,0,0,0.5);
border-radius: 30rpx; border-radius: 30rpx;
@ -15,16 +15,16 @@ page{
color:#fff; color:#fff;
} }
.body{ .body{
border-radius: 32rpx 32rpx 0 0;
background-color:#f1f8f6; background-color:#f1f8f6;
width: 750rpx; width: 750rpx;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 600rpx; min-height: 500rpx;
position: absolute;
top: 520rpx;
.top{ .top{
position: sticky;
position: -webkit-sticky;
top: 0;
padding:40rpx 30rpx 30rpx 30rpx; padding:40rpx 30rpx 30rpx 30rpx;
border-radius: 32rpx; border-radius: 32rpx;
background-color: #fff; background-color: #fff;

@ -1,8 +1,8 @@
import {FC, useEffect, useState} from "react"; import {FC, useCallback, useEffect, useState} from "react";
import {Image, Swiper, SwiperItem, Text, Video, View} from "@tarojs/components"; import {Image, Swiper, SwiperItem, Text, Video, View} from "@tarojs/components";
import {ArticleRecord, brandApi, BrandRecord} from "@/api"; import {ArticleRecord, brandApi, BrandRecord} from "@/api";
import styles from './info.module.scss' import styles from './info.module.scss'
import Taro, {useRouter} from "@tarojs/taro"; import Taro, {useReachBottom, useRouter} from "@tarojs/taro";
import LineEllipsis from "@/components/textCollapse/collapse"; import LineEllipsis from "@/components/textCollapse/collapse";
import Empty from "@/components/empty/empty"; import Empty from "@/components/empty/empty";
@ -12,10 +12,19 @@ type Params = {
const BrandInfo: FC = () => { const BrandInfo: FC = () => {
const {id} = useRouter().params as unknown as Params const {id} = useRouter().params as unknown as Params
const [brandInfo, setBrandInfo] = useState<BrandRecord>() const [brandInfo, setBrandInfo] = useState<BrandRecord>()
const [articleList, setArticleList] = useState<ArticleRecord[]>() const [articleList, setArticleList] = useState<ArticleRecord[]>([])
const [curIndex,setCurIndex] = useState<number>(1) const [curIndex,setCurIndex] = useState<number>(1)
const [page, setPage] = useState(1)
const [total, setTotal] = useState(0)
useEffect(() => { useEffect(() => {
Taro.showLoading({
title: '加载中',
mask: true
})
setTimeout(function () {
Taro.hideLoading()
}, 1000)
getData() getData()
}, [id]) }, [id])
@ -24,26 +33,48 @@ const BrandInfo: FC = () => {
const data = await brandApi.info(id) const data = await brandApi.info(id)
Taro.setNavigationBarTitle({title: data.name}) Taro.setNavigationBarTitle({title: data.name})
setBrandInfo(data) setBrandInfo(data)
const data1 = await brandApi.articleList(id) const data1 = await brandApi.articleList(id,page)
setArticleList(data1.list) setTotal(data1.total)
setArticleList([
...(articleList || []),
...data1.list
])
} catch (e) { } catch (e) {
// setBrandInfo({disabled: 0, graphic_introduction: "", id: 0, introductory_video: "", name: "", brand_album:['1','2','3']}) // setBrandInfo({disabled: 0, graphic_introduction: "", id: 0, introductory_video: "", name: "", brand_album:['1','2','3']})
} }
} }
useReachBottom(useCallback(() => {
if (articleList?.length < total) {
setPage(page + 1)
}
}, [total, articleList]))
useEffect(() => {
brandApi.articleList(id,page).then(res => {
setTotal(res.total)
setArticleList([
...(articleList || []),
...res.list
])
})
}, [page])
function onChange(e){ function onChange(e){
console.log(e) console.log(e)
setCurIndex(+e.detail.current+1) setCurIndex(+e.detail.current+1)
} }
return ( return (
<View className='flex flex-column '> <View className='flex flex-column' style={{display:brandInfo?'flex':'none'}}>
<Swiper <Swiper
className={styles['swiper']} className={styles['swiper']}
indicatorColor='#999' indicatorColor='#999'
indicatorActiveColor='#333' indicatorActiveColor='#333'
indicatorDots indicatorDots={false}
onChange={onChange} onChange={onChange}
> >
{ brandInfo?.introductory_video_resource?.url && <SwiperItem> { brandInfo?.introductory_video_resource?.url && <SwiperItem>
<Video <Video
@ -93,92 +124,6 @@ const BrandInfo: FC = () => {
) )
: <Empty name='空空如也'/> : <Empty name='空空如也'/>
} }
{
articleList?.length ?
articleList.map((i: any) =>
<View className={styles.box} onClick={() => {
Taro.navigateTo({url: `/pages/preview/brand/article/article?id=${i.id}`})
}}>
<View className={styles.inner}>
<View className={styles.leftBox}>
<View className='font-weight mb-2 font-28 lh-40'>{i.title}</View>
<View className={styles.desc}>{i.created_at} {i.page_view}</View>
</View>
{/*<Image mode='aspectFill' className={styles.image} src={'dd'}/>*/}
</View>
</View>
)
: <Empty name='空空如也'/>
}
{
articleList?.length ?
articleList.map((i: any) =>
<View className={styles.box} onClick={() => {
Taro.navigateTo({url: `/pages/preview/brand/article/article?id=${i.id}`})
}}>
<View className={styles.inner}>
<View className={styles.leftBox}>
<View className='font-weight mb-2 font-28 lh-40'>{i.title}</View>
<View className={styles.desc}>{i.created_at} {i.page_view}</View>
</View>
{/*<Image mode='aspectFill' className={styles.image} src={'dd'}/>*/}
</View>
</View>
)
: <Empty name='空空如也'/>
}
{
articleList?.length ?
articleList.map((i: any) =>
<View className={styles.box} onClick={() => {
Taro.navigateTo({url: `/pages/preview/brand/article/article?id=${i.id}`})
}}>
<View className={styles.inner}>
<View className={styles.leftBox}>
<View className='font-weight mb-2 font-28 lh-40'>{i.title}</View>
<View className={styles.desc}>{i.created_at} {i.page_view}</View>
</View>
{/*<Image mode='aspectFill' className={styles.image} src={'dd'}/>*/}
</View>
</View>
)
: <Empty name='空空如也'/>
}
{
articleList?.length ?
articleList.map((i: any) =>
<View className={styles.box} onClick={() => {
Taro.navigateTo({url: `/pages/preview/brand/article/article?id=${i.id}`})
}}>
<View className={styles.inner}>
<View className={styles.leftBox}>
<View className='font-weight mb-2 font-28 lh-40'>{i.title}</View>
<View className={styles.desc}>{i.created_at} {i.page_view}</View>
</View>
{/*<Image mode='aspectFill' className={styles.image} src={'dd'}/>*/}
</View>
</View>
)
: <Empty name='空空如也'/>
}
{
articleList?.length ?
articleList.map((i: any) =>
<View className={styles.box} onClick={() => {
Taro.navigateTo({url: `/pages/preview/brand/article/article?id=${i.id}`})
}}>
<View className={styles.inner}>
<View className={styles.leftBox}>
<View className='font-weight mb-2 font-28 lh-40'>{i.title}</View>
<View className={styles.desc}>{i.created_at} {i.page_view}</View>
</View>
{/*<Image mode='aspectFill' className={styles.image} src={'dd'}/>*/}
</View>
</View>
)
: <Empty name='空空如也'/>
}
</View> </View>
</View> </View>
</View> </View>

@ -1,5 +1,30 @@
.botmBox{
z-index: 99;
position: fixed;
bottom: 0;
width: 750rpx;
box-sizing: border-box;
height: 180rpx;
padding-bottom: 60rpx;
display: flex;
justify-content: center;
align-items: center;
background: #F5F8F7;
view{
width: 560rpx;
height: 76rpx;
background: #45D4A8;
border-radius: 38rpx 38rpx 38rpx 38rpx;
color: #fff;
font-weight: 500;
font-size: 40rpx;
text-align: center;
line-height: 76rpx;
}
}
.fixedBox{ .fixedBox{
position: fixed; position: fixed;
z-index: 100;
top:0; top:0;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;

@ -1,16 +1,22 @@
import {FC, useEffect, useState} from "react"; import {FC, useEffect, useMemo, useState} from "react";
import {Image, Text, View} from "@tarojs/components"; import {Image, PageContainer, Text, View} from "@tarojs/components";
import Taro, {useRouter} from "@tarojs/taro"; import Taro, {useRouter} from "@tarojs/taro";
import {ArticleRecord, brandApi} from "@/api"; import {ArticleRecord, brandApi} from "@/api";
import styles from './article.module.scss' import styles from './article.module.scss'
import down from '@/static/img/doubleDown.png' import down from '@/static/img/doubleDown.png'
import {Profile} from "@/store"; import {Profile} from "@/store";
import {parse} from "@/utils/marked/marked";
const article:FC = () => { const article:FC = () => {
const {token} = Profile.useContainer() const {token, empty} = Profile.useContainer()
const {id} = useRouter().params as unknown as { id: number} const {id} = useRouter().params as unknown as { id: number}
const [show,setShow] = useState(false)
const [articleInfo,setArticleInfo] = useState<ArticleRecord>() const [articleInfo,setArticleInfo] = useState<ArticleRecord>()
const { children, headings } = useMemo(() => parse(articleInfo?.content || ''), [articleInfo])
console.log(headings,'headings')
const query = Taro.createSelectorQuery()
useEffect(() => { useEffect(() => {
getData() getData()
}, [id]) }, [id])
@ -24,11 +30,33 @@ const article:FC = () => {
} catch (e) { } catch (e) {
} }
} }
function mao(id: string){
console.log(id)
setShow(false)
Taro.nextTick(() => {
query.select(`#${id}`).boundingClientRect()
query.exec((res) => {
if(res.length){
Taro.pageScrollTo({
scrollTop: res[res.length-1].top,
duration: 300}
)
}
})
})
}
function helloWorld() { function helloWorld() {
const html = articleInfo?.content;
return ( return (
<> <>
<View style={{padding:'10px',height:!token ? Taro.getWindowInfo().windowHeight-60+'px' : 'auto',overflow:!token ? 'hidden':'auto'}} dangerouslySetInnerHTML={{ __html: html! }}></View> {/*<View style={{padding:'10px'}}>*/}
{/* { children }*/}
{/*</View>*/}
<View className={styles.botmBox} style={{display:show?'none':'flex'}} onClick={()=>{setShow(true)}}>
<View></View>
</View>
<View style={{padding:'10px',height:!token ? Taro.getWindowInfo().windowHeight-60+'px' : 'auto',overflow:!token ? 'hidden':'auto'}}>
{ children }
</View>
{ {
!token && !token &&
<View className={styles.fixedBox}> <View className={styles.fixedBox}>
@ -36,13 +64,23 @@ const article:FC = () => {
<View className={styles['fixedBox-inner-icon']}> <View className={styles['fixedBox-inner-icon']}>
<Image src={down}></Image> <Image src={down}></Image>
</View> </View>
<View className={styles['fixedBox-inner-box']}> <View className={styles['fixedBox-inner-box']} onClick={empty}>
<Text></Text> <Text></Text>
</View> </View>
</View> </View>
</View> </View>
} }
<PageContainer show={show} round={true} overlay={true} overlayStyle={'background:rgba(0,0,0,0.3)'} >
<View className="px-3 py-5">
{headings.length > 0 &&
headings.map((d) =>
<View className="pb-3" style={{fontSize:'28rpx',fontWeight: '500',color: '#323635'}} onClick={()=>{mao(d.id)}}>{d.text}</View>
)
}
</View>
</PageContainer>
</> </>
) )
} }

@ -11,8 +11,17 @@ const BrandList: FC = () => {
const [page, setPage] = useState(1) const [page, setPage] = useState(1)
const [brands, setBrands] = useState<any[]>([]) const [brands, setBrands] = useState<any[]>([])
const [total, setTotal] = useState(0) const [total, setTotal] = useState(0)
const [fetchDone,setFetchDone] = useState(false)
useEffect(() => { useEffect(() => {
Taro.showLoading({
title: '加载中',
mask: true
})
setTimeout(function () {
Taro.hideLoading()
setFetchDone(true)
}, 800)
getData() getData()
}, [page]) }, [page])
@ -37,7 +46,7 @@ const BrandList: FC = () => {
return ( return (
<View className='p-2'> <View className='p-2' style={{display:fetchDone?'block':'none'}}>
{ {
brands.length > 0 ? brands.length > 0 ?
brands.map((d) => brands.map((d) =>

@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '链接',
onReachBottomDistance: 30
})

@ -0,0 +1,9 @@
import {FC} from "react";
import {WebView} from "@tarojs/components";
import {useRouter} from "@tarojs/taro";
const Web:FC = () => {
const {url} = useRouter().params as unknown as { url: string }
return <WebView src={url}></WebView>
}
export default Web

@ -0,0 +1,30 @@
import { FC, ReactNode} from "react";
import {View} from "@tarojs/components";
import Taro from "@tarojs/taro";
interface TabLinkProps{
className:string
url:string
children: ReactNode[]
}
const TabLink:FC<TabLinkProps> = (props:TabLinkProps) => {
function linkTab(){
console.log(props.url)
let webExp= new RegExp(/http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- .\/?%&=]*)?/)
if(webExp.test(props.url)){
//跳转webview
Taro.navigateTo({url:`/pages/preview/webView/webView?url=${props.url}`})
}else{
//跳转小程序页面
}
}
return (
<View onClick={linkTab} className="font-28" style={{display:"inline-block",color:'#45D4A8'}}>22222</View>
)
}
export default TabLink

@ -0,0 +1,174 @@
.code {}
.h1 {
font-size: 48rpx;
margin-top: 20rpx;
margin-bottom: 40rpx;
}
.h2 {
font-size: 44rpx;
margin-top: 20rpx;
margin-bottom: 40rpx;
}
.h3 {
font-size: 40rpx;
margin-top: 20rpx;
margin-bottom: 40rpx;
}
.h4 {
font-size: 36rpx;
margin-top: 20rpx;
margin-bottom: 40rpx;
}
.h5 {
font-size: 32rpx;
margin-top: 20rpx;
margin-bottom: 40rpx;
}
.h6 {
font-size: 28rpx;
margin-top: 20rpx;
margin-bottom: 40rpx;
}
.hr {
border-bottom: 1px solid #aaa;
margin-top: 12rpx;
margin-bottom: 12rpx;
}
.blockquote {
border-left: 8rpx solid #aaa;
padding: 16rpx;
}
.list,
.taskList,
.orderedList {
display: block;
margin-bottom: 12rpx;
}
.listItem,
.taskListItem,
.orderedListItem {
padding-left: 1rem;
position: relative;
}
.taskListItem {
padding-left: 1.5rem;
}
.checkedTaskIndicator,
.uncheckTaskIndicator,
.listItemIndicator {
position: absolute;
top: 0;
left: 0;
user-select: none;
}
.checkedTaskIndicator,
.uncheckTaskIndicator {
width: 1rem;
height: 1rem;
overflow: hidden;
border: 2px solid #ddd;
display: flex;
justify-content: center;
align-items: center;
font-size: 0.65rem;
border-radius: 8rpx;
color: #999;
}
.paragraph {
margin-top: 4rpx;
margin-bottom: 12rpx;
font-size: 28rpx;
font-weight: 500;
color: #444444;
line-height: 35rpx;
}
.blockHtml {}
.inlineHtml {}
.text {
}
.def {}
.strong {
font-weight: bold;
}
.em {
font-style: italic;
}
.del {
text-decoration: line-through;
}
.codespan {
padding: 0 12rpx;
font-size: 85%;
border: 1px solid #aaa;
background-color: #ccc;
border-radius: 8rpx;
}
//.escape {}
//.br {}
.link {
display: inline;
text-decoration: underline;
color: blue;
// todo 取消点击时的背景
}
.image {
display: inline-block;
max-width: 100%;
height: auto;
}
.video {
display: block;
max-width: 100%;
}
// todo 自动换行
.table {
max-width: 100%;
overflow-x: auto;
border: 1px solid #dee2e6;
}
.tableBodyRow,
.tableHeader {
display: flex;
&:not(:last-child) {
border-bottom: 1px solid #dee2e6;
}
}
.tableHeaderCell,
.tableBodyCell {
flex: 1;
padding: 8rpx 12rpx;
&:not(:last-child) {
border-right: 1px solid #dee2e6;
}
}

@ -0,0 +1,319 @@
import {Token, Tokens, lexer} from "marked"
import {Attributes, ComponentType, createElement, ReactElement, ReactNode} from "react"
import {View, Text, Image, Video, Audio} from "@tarojs/components"
import {parseHeadingId, walkTokens, unescape} from "./utils"
import TabLink from "./components/Tablink";
import './marked.scss'
/**
* Markdown
*
* ```ts
* const Markdown: FC<{ source: string }> = ({ source }) => {
* const { children, headings } = useMemo(() => parse(source), [source])
* console.log(headings) // => [{level: 1, text: 'heading', id: 'custom-id'}]
* return (<View>{children}</View>)
* }
*
* <Markdown source={'# heading {#custom-id}'} />
* ```
*
* @param {string} content
*
* @see {@link https://www.markdownguide.org}
* @see {@link https://marked.js.org}
*/
export const parse = (content: string) => {
const headings: Heading[] = []
let nextKey = 0
const ctx: RenderContext = {
createReactElement<P>(type: ComponentType<P>, props?: Props<P>): ReactElement<P> {
props ??= {} as Props<P>
props.key ??= ++nextKey
return createElement(type, props)
},
ensureHeading(token: Tokens.Heading): string {
// todo(hupeh): id 不能重复
// todo(hupeh): 第一层标题级别必须一致
const id = parseHeadingId(token)
headings.push({id, text: token.text, level: token.depth})
return id
},
}
const tokens = lexer(content)
const children = render(ctx, tokens)
return {children, headings}
}
export interface Heading {
level: number
text: string
id: string
}
type Props<P> = Attributes & P
interface RenderContext {
createReactElement<P>(type: ComponentType<P>, props?: Props<P>): ReactElement<P>
ensureHeading(token: Tokens.Heading): string
}
function render(ctx: RenderContext, tokens: Token[]) {
return walkTokens<ReactNode>(tokens, (token) => renderToken(ctx, token))
}
function renderToken(ctx: RenderContext, token: Token) {
return (renderer[token.type] ?? renderer.generic)(ctx, token)
}
const renderer = {
space(_: RenderContext, tk: Tokens.Space) {
// todo(hupeh): 测试是否需要 Text 实现
return tk.raw
},
code(ctx: RenderContext, tk: Tokens.Code) {
return ctx.createReactElement(View, {
className: 'code',
children: tk.escaped ? escape(tk.text) : tk.text,
})
},
heading(ctx: RenderContext, tk: Tokens.Heading) {
const id = ctx.ensureHeading(tk)
// todo(hupeh): 是否需要调整标题级别保证顺序一致
return ctx.createReactElement(View, {
id,
className: `h${tk.depth}`,
children: render(ctx, tk.tokens),
})
},
hr(ctx: RenderContext, _: Tokens.Hr) {
return ctx.createReactElement(View, {
className: 'hr',
})
},
blockquote(ctx: RenderContext, tk: Tokens.Blockquote) {
return ctx.createReactElement(View, {
className: 'blockquote',
children: render(ctx, tk.tokens),
})
},
list(ctx: RenderContext, tk: Tokens.List) {
const isTaskList = tk.items[0]?.task == true;
// todo(hupeh): 什么是 tk.loose
return ctx.createReactElement(View, {
className: isTaskList ? 'taskList' : tk.ordered ? 'orderedList' : 'list',
children: tk.items.map((item, index) => {
return ctx.createReactElement(View, {
className: isTaskList ? 'taskListItem' : tk.ordered ? 'orderedListItem' : "listItem",
children: [
!isTaskList ? ctx.createReactElement(View, {
className: 'listItemIndicator',
children: tk.ordered ? index + 1 : '•',
}) : null,
item.task ? ctx.createReactElement(View, {
className: item.checked ? 'checkedTaskIndicator' : 'uncheckTaskIndicator',
children: item.checked ? ctx.createReactElement(Text, {
children: '✔',
}) : null,
}) : null,
...render(ctx, item.tokens),
],
});
})
});
},
paragraph(ctx: RenderContext, tk: Tokens.Paragraph) {
// todo(hupeh): tk.pre
return ctx.createReactElement(View, {
className: 'paragraph',
children: render(ctx, tk.tokens),
});
},
html(ctx: RenderContext, tk: Tokens.HTML) {
// todo(hupeh): 什么是 tk.pre ? 对应 <pre> 标签吗?
// if (tk.pre) {}
if (tk.block) {
return ctx.createReactElement(View, {
className: 'blockHtml',
children: tk.text.split(/\r?\n/).map(line => {
return ctx.createReactElement(View, {
className: 'blockHtmlItem',
children: ctx.createReactElement(Text, {
space: 'nbsp', // 空格符号渲染方式
decode: true, // 正确显示实体
children: line.trimEnd(),
}),
})
}),
})
}
return ctx.createReactElement(Text, {
className: 'inlineHtml',
children: tk.text,
})
},
text(ctx: RenderContext, tk: Tokens.Text) {
if (tk.tokens?.length) {
return render(ctx, tk.tokens)
}
// todo 转义 HTML 实体
return ctx.createReactElement(Text, {
className: 'text',
children: unescape(tk.text),
decode: true,
})
},
def(ctx: RenderContext, tk: Tokens.Def) {
return ctx.createReactElement(Text, {
className: 'def',
children: tk.title,
})
},
escape(ctx: RenderContext, tk: Tokens.Escape) {
return ctx.createReactElement(Text, {
className: 'escape',
children: escape(tk.text),
})
},
link(ctx: RenderContext, tk: Tokens.Link) {
// todo(hupeh): 页面内部跳转,根据 heading 的 ID 实现
return ctx.createReactElement(TabLink, {
className: 'link',
url: tk.href,
children: render(ctx, tk.tokens),
})
},
image(ctx: RenderContext, tk: Tokens.Image) {
// todo(hupeh): 相关控制属性可以通过 QueryString 解析出来
const ext = tk.href.split('?').shift()!.split('.').pop()!.toLowerCase();
switch (ext) {
case 'mp4':
case 'm4v':
case 'mov':
case 'qt':
case 'avi':
case 'flv':
case 'wmv':
case 'asf':
case 'mkv':
case 'rm':
case 'rmvb':
case 'vob':
case 'ts':
case 'dat':
return ctx.createReactElement(Video, {
className: 'video',
src: tk.href,
})
case 'mp3':
case 'wma':
case 'flac':
case 'wav':
// todo(hupeh): 使用 Taro.createInnerAudioContext 接口封装组件
return ctx.createReactElement(Audio, {
className: 'audio',
src: tk.href
})
default:
return ctx.createReactElement(Image, {
className: 'image',
src: tk.href,
mode: "widthFix",
webp: ext === 'web',
})
}
},
strong(ctx: RenderContext, tk: Tokens.Strong) {
return ctx.createReactElement(Text, {
className: 'strong',
children: render(ctx, tk.tokens),
})
},
em(ctx: RenderContext, tk: Tokens.Em) {
return ctx.createReactElement(Text, {
className: 'em',
children: render(ctx, tk.tokens),
})
},
codespan(ctx: RenderContext, tk: Tokens.Codespan) {
return ctx.createReactElement(Text, {
className: 'codespan',
children: tk.text,
})
},
br(ctx: RenderContext, _: Tokens.Br) {
return ctx.createReactElement(View, {
className: 'br',
})
},
del(ctx: RenderContext, tk: Tokens.Del) {
return ctx.createReactElement(Text, {
className: 'del',
children: render(ctx, tk.tokens),
})
},
table(ctx: RenderContext, tk: Tokens.Table) {
// todo(hupeh): 使用自定义 table 组件,检测单元格宽度
// return "暂不支持表格渲染";
return ctx.createReactElement(View, {
className: 'table',
children: [
ctx.createReactElement(View, {
className: 'tableHeader',
children: tk.header.map((cell, index) => {
return ctx.createReactElement(View, {
className: 'tableHeaderCell',
style: {textAlign: tk.align[index] || 'left'},
children: render(ctx, cell.tokens),
})
}),
}),
ctx.createReactElement(View, {
className: 'tableBody',
children: tk.rows.map(rows => {
return ctx.createReactElement(View, {
className: 'tableBodyRow',
children: rows.map((cell, index) => {
return ctx.createReactElement(View, {
className: 'tableBodyCell',
style: {textAlign: tk.align[index] || 'left'},
children: render(ctx, cell.tokens),
})
})
})
}),
}),
],
})
},
generic(ctx: RenderContext, tk: Tokens.Generic) {
// todo(hupeh): 验证 tk.type
// todo(hupeh): 调试模式下不返回 null
return tk.tokens?.length ? render(ctx, tk.tokens) : null
},
}

@ -0,0 +1,86 @@
import {Token, Tokens, TokensList} from "@/utils/marked/marked"
import {Attributes, ComponentType, createElement, ReactElement} from "react";
const unescapeTest = /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig
export function unescape(html: string) {
// explicitly match decimal, hex, and named HTML entities
return html.replace(unescapeTest, (_, n) => {
n = n.toLowerCase()
if (n === 'colon') return ':'
if (n.charAt(0) === '#') {
return n.charAt(1) === 'x'
? String.fromCharCode(parseInt(n.substring(2), 16))
: String.fromCharCode(+n.substring(1))
}
return ''
})
}
// This alphabet uses `A-Za-z0-9_-` symbols.
// The order of characters is optimized for better gzip and brotli compression.
// Same as in non-secure/index.js
const urlAlphabet = 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict'
export function hash(str: string) {
const parts = Math.abs(str.split("")
.reduce((a, b) => {
a = ((a << 5) - a) + b.charCodeAt(0);
return a & a;
}, 0))
.toString()
.split('')
let id = ''
// We are reading directly from the random pool to avoid creating new array
for (const part of parts) {
const size = Number.parseInt(part)
if (!Number.isNaN(size)) {
// It is incorrect to use bytes exceeding the alphabet size.
// The following mask reduces the random byte in the 0-255 value
// range to the 0-63 value range. Therefore, adding hacks, such
// as empty string fallback or magic numbers, is unneccessary because
// the bitmask trims bytes down to the alphabet size.
id += urlAlphabet[size & 63]
}
}
return id.replace(/^-+|-+$/g, '')
}
export function walkTokens<T = void>(tokens: Token[] | TokensList, callback: (token: Token) => T | T[]) {
let values: T[] = [];
for (const token of tokens) {
values = values.concat(callback.call(this, token))
}
return values
}
export function parseHeadingId(tk: Tokens.Heading) {
const index = tk.tokens.length - 1;
if (tk.tokens[index].type !== 'text') {
return hash(tk.raw)
}
// https://github.com/markedjs/marked-custom-heading-id/blob/main/src/index.js
const child = tk.tokens[index] as Tokens.Text
const headingIdRegex = /(?: +|^)\{#([a-z][\w-]*)\}(?: +|$)/i;
const hasId = child.text.match(headingIdRegex);
if (!hasId) {
return hash(tk.raw)
}
child.text = child.text.replace(headingIdRegex, '')
tk.text = tk.text.replace(headingIdRegex, '')
return hasId[1]
}
let nextKey = 0
export function h<Props>(type: ComponentType<Props>, props?: Attributes & Props): ReactElement<Props> {
props ??= {} as Attributes & Props
props.key ??= ++nextKey
return createElement(type, props)
}
Loading…
Cancel
Save