diff --git a/apps/miniprogram/package.json b/apps/miniprogram/package.json
index f8279ab..f12c2c6 100644
--- a/apps/miniprogram/package.json
+++ b/apps/miniprogram/package.json
@@ -34,6 +34,7 @@
"@tarojs/runtime": "4.2.0",
"@tarojs/shared": "4.2.0",
"@tarojs/taro": "4.2.0",
+ "mp-html": "^2.5.2",
"react": "^18.3.0",
"react-dom": "18.3.1",
"zustand": "^5.0.0"
diff --git a/apps/miniprogram/pnpm-lock.yaml b/apps/miniprogram/pnpm-lock.yaml
index 19bcf03..b96514b 100644
--- a/apps/miniprogram/pnpm-lock.yaml
+++ b/apps/miniprogram/pnpm-lock.yaml
@@ -38,6 +38,9 @@ importers:
'@tarojs/taro':
specifier: 4.2.0
version: 4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.28)(html-webpack-plugin@5.6.7(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)))(postcss@8.5.12)(rollup@3.30.0)(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)))(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.28)(html-webpack-plugin@5.6.7(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)))(postcss@8.5.12)(rollup@3.30.0)(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)))(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
+ mp-html:
+ specifier: ^2.5.2
+ version: 2.5.2
react:
specifier: ^18.3.0
version: 18.3.1
@@ -4435,6 +4438,9 @@ packages:
mobile-detect@1.4.5:
resolution: {integrity: sha512-yc0LhH6tItlvfLBugVUEtgawwFU2sIe+cSdmRJJCTMZ5GEJyLxNyC/NIOAOGk67Fa8GNpOttO3Xz/1bHpXFD/g==}
+ mp-html@2.5.2:
+ resolution: {integrity: sha512-45e8c32Qgux4YU4iC3qCSFsOh3y+RwPwZ+iz/vvLkDgSGWk+1zsL4WUzWWQc9w3AsAfkaD/QR0oIufIDngBmXA==}
+
ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
@@ -11011,6 +11017,8 @@ snapshots:
mobile-detect@1.4.5: {}
+ mp-html@2.5.2: {}
+
ms@2.0.0: {}
ms@2.1.3: {}
diff --git a/apps/miniprogram/src/components/RichArticle/index.tsx b/apps/miniprogram/src/components/RichArticle/index.tsx
new file mode 100644
index 0000000..95a2542
--- /dev/null
+++ b/apps/miniprogram/src/components/RichArticle/index.tsx
@@ -0,0 +1,32 @@
+import { memo, useMemo } from 'react';
+import { View } from '@tarojs/components';
+import { sanitizeHtml } from '@/utils/sanitize-html';
+
+interface RichArticleProps {
+ html: string;
+ className?: string;
+}
+
+function prepareHtml(raw: string): string {
+ return sanitizeHtml(raw);
+}
+
+function RichArticle({ html, className }: RichArticleProps) {
+ const content = useMemo(() => prepareHtml(html), [html]);
+
+ if (!content) return null;
+
+ return (
+
+
+
+ );
+}
+
+export default memo(RichArticle);
diff --git a/apps/miniprogram/src/native-components/mp-html/index.js b/apps/miniprogram/src/native-components/mp-html/index.js
new file mode 100644
index 0000000..6ad17c0
--- /dev/null
+++ b/apps/miniprogram/src/native-components/mp-html/index.js
@@ -0,0 +1,8 @@
+"use strict";function e(t){"@babel/helpers - typeof";return(e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(t)}function t(e,t,o){return(t=n(t))in e?Object.defineProperty(e,t,{value:o,enumerable:!0,configurable:!0,writable:!0}):e[t]=o,e}function n(t){var n=o(t,"string");return"symbol"==e(n)?n:n+""}function o(t,n){if("object"!=e(t)||!t)return t;var o=t[Symbol.toPrimitive];if(void 0!==o){var i=o.call(t,n||"default");if("object"!=e(i))return i;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===n?String:Number)(t)}/*!
+ * mp-html v2.5.2
+ * https://github.com/jin-yufeng/mp-html
+ *
+ * Released under the MIT license
+ * Author: Jin Yufeng
+ */
+var i=require("./parser"),r=[];Component({data:{nodes:[]},properties:{containerStyle:String,content:{type:String,value:"",observer:function(e){this.setContent(e)}},copyLink:{type:Boolean,value:!0},domain:String,errorImg:String,lazyLoad:Boolean,loadingImg:String,pauseVideo:{type:Boolean,value:!0},previewImg:{type:null,value:!0},scrollTable:Boolean,selectable:null,setTitle:{type:Boolean,value:!0},showImgMenu:{type:Boolean,value:!0},tagStyle:Object,useAnchor:null},created:function(){this.plugins=[];for(var e=r.length;e--;)this.plugins.push(new r[e](this))},detached:function(){this._hook("onDetached")},methods:{in:function(e,t,n){e&&t&&n&&(this._in={page:e,selector:t,scrollTop:n})},navigateTo:function(e,n){var o=this;return new Promise(function(i,r){if(!o.data.useAnchor)return void r(Error("Anchor is disabled"));var a=wx.createSelectorQuery().in(o._in?o._in.page:o).select((o._in?o._in.selector:"._root")+(e?"".concat(">>>","#").concat(e):"")).boundingClientRect();o._in?a.select(o._in.selector).scrollOffset().select(o._in.selector).boundingClientRect():a.selectViewport().scrollOffset(),a.exec(function(e){if(!e[0])return void r(Error("Label not found"));var a=e[1].scrollTop+e[0].top-(e[2]?e[2].top:0)+(n||parseInt(o.data.useAnchor)||0);o._in?o._in.page.setData(t({},o._in.scrollTop,a)):wx.pageScrollTo({scrollTop:a,duration:300}),i()})})},getText:function(e){var t="";return function e(n){for(var o=0;o"0"&&i.name[1]<"7";r&&t&&"\n"!==t[t.length-1]&&(t+="\n"),i.children&&e(i.children),r&&"\n"!==t[t.length-1]?t+="\n":"td"!==i.name&&"th"!==i.name||(t+="\t")}}}(e||this.data.nodes),t},getRect:function(){var e=this;return new Promise(function(t,n){wx.createSelectorQuery().in(e).select("._root").boundingClientRect().exec(function(e){return e[0]?t(e[0]):n(Error("Root label not found"))})})},pauseMedia:function(){for(var e=(this._videos||[]).length;e--;)this._videos[e].pause()},setPlaybackRate:function(e){this.playbackRate=e;for(var t=(this._videos||[]).length;t--;)this._videos[t].playbackRate(e)},setContent:function(e,t){var n=this;this.imgList&&t||(this.imgList=[]),this._videos=[];var o={},r=new i(this).parse(e);if(t)for(var a=this.data.nodes.length,s=r.length;s--;)o["nodes[".concat(a+s,"]")]=r[s];else o.nodes=r;if(this.setData(o,function(){n._hook("onLoad"),n.triggerEvent("load")}),this.data.lazyLoad||this.imgList._unloadimgs
\ No newline at end of file
diff --git a/apps/miniprogram/src/native-components/mp-html/index.wxss b/apps/miniprogram/src/native-components/mp-html/index.wxss
new file mode 100644
index 0000000..5f74d06
--- /dev/null
+++ b/apps/miniprogram/src/native-components/mp-html/index.wxss
@@ -0,0 +1 @@
+._root{padding:1px 0;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch}._select{-webkit-user-select:text;user-select:text}
\ No newline at end of file
diff --git a/apps/miniprogram/src/native-components/mp-html/node/node.js b/apps/miniprogram/src/native-components/mp-html/node/node.js
new file mode 100644
index 0000000..4257f3c
--- /dev/null
+++ b/apps/miniprogram/src/native-components/mp-html/node/node.js
@@ -0,0 +1 @@
+"use strict";function t(t,e){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);e&&(o=o.filter(function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable})),r.push.apply(r,o)}return r}function e(e){for(var o=1;o=t.length?{done:!0}:{done:!1,value:t[o++]}},e:function(t){throw t},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var n,a=!0,c=!1;return{s:function(){r=r.call(t)},n:function(){var t=r.next();return a=t.done,t},e:function(t){c=!0,n=t},f:function(){try{a||null==r.return||r.return()}finally{if(c)throw n}}}}function s(t,e){if(t){if("string"==typeof t)return c(t,e);var r={}.toString.call(t).slice(8,-1);return"Object"===r&&t.constructor&&(r=t.constructor.name),"Map"===r||"Set"===r?Array.from(t):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?c(t,e):void 0}}function c(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,o=Array(e);ro.length;)0!==Object.keys(t[s-1]).length&&(r[i+"["+(s-1)+"]"]={}),s-=1;for(;so.src.length&&(i=0),ivar e={abbr:!0,b:!0,big:!0,code:!0,del:!0,em:!0,i:!0,ins:!0,label:!0,q:!0,small:!0,span:!0,strong:!0,sub:!0,sup:!0};module.exports=function(n,i){return e[n]||-1!==(i||"").indexOf("inline")};{{n.text}}{{'\n'}}
\ No newline at end of file
diff --git a/apps/miniprogram/src/native-components/mp-html/node/node.wxss b/apps/miniprogram/src/native-components/mp-html/node/node.wxss
new file mode 100644
index 0000000..43a2fab
--- /dev/null
+++ b/apps/miniprogram/src/native-components/mp-html/node/node.wxss
@@ -0,0 +1 @@
+._a{padding:1.5px 0 1.5px 0;color:#366092;word-break:break-all}._hover{text-decoration:underline;opacity:.7}._img{max-width:100%;-webkit-touch-callout:none}._b,._strong{font-weight:700}._code{font-family:monospace}._del{text-decoration:line-through}._em,._i{font-style:italic}._h1{font-size:2em}._h2{font-size:1.5em}._h3{font-size:1.17em}._h5{font-size:.83em}._h6{font-size:.67em}._h1,._h2,._h3,._h4,._h5,._h6{display:block;font-weight:700}._ins{text-decoration:underline}._li{display:list-item}._ol{list-style-type:decimal}._ol,._ul{display:block;padding-left:40px;margin:1em 0}._q::before{content:'"'}._q::after{content:'"'}._sub{font-size:smaller;vertical-align:sub}._sup{font-size:smaller;vertical-align:super}._tbody,._tfoot,._thead{display:table-row-group}._tr{display:table-row}._td,._th{display:table-cell;vertical-align:middle}._th{font-weight:700;text-align:center}._ul{list-style-type:disc}._ul ._ul{margin:0;list-style-type:circle}._ul ._ul ._ul{list-style-type:square}._abbr,._b,._code,._del,._em,._i,._ins,._label,._q,._span,._strong,._sub,._sup{display:inline}._blockquote,._div,._p{display:block}
\ No newline at end of file
diff --git a/apps/miniprogram/src/native-components/mp-html/parser.js b/apps/miniprogram/src/native-components/mp-html/parser.js
new file mode 100644
index 0000000..d2b12b3
--- /dev/null
+++ b/apps/miniprogram/src/native-components/mp-html/parser.js
@@ -0,0 +1 @@
+"use strict";function t(t,e){var s="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(!s){if(Array.isArray(t)||(s=i(t))||e&&t&&"number"==typeof t.length){s&&(t=s);var n=0,a=function(){};return{s:a,n:function(){return n>=t.length?{done:!0}:{done:!1,value:t[n++]}},e:function(t){throw t},f:a}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var r,o=!0,l=!1;return{s:function(){s=s.call(t)},n:function(){var t=s.next();return o=t.done,t},e:function(t){l=!0,r=t},f:function(){try{o||null==s.return||s.return()}finally{if(l)throw r}}}}function i(t,i){if(t){if("string"==typeof t)return e(t,i);var s={}.toString.call(t).slice(8,-1);return"Object"===s&&t.constructor&&(s=t.constructor.name),"Map"===s||"Set"===s?Array.from(t):"Arguments"===s||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(s)?e(t,i):void 0}}function e(t,i){(null==i||i>t.length)&&(i=t.length);for(var e=0,s=Array(i);e=-1;e--)(-1===e||t[e].c||!t[e].name||"div"!==t[e].name&&"p"!==t[e].name&&"h"!==t[e].name[0]||(t[e].attrs.style||"").includes("inline"))&&(i-e>=5&&t.splice(e+1,i-e,{name:"div",attrs:{},children:t.slice(e+1,i+1)}),i=e-1)}function r(t){this.options=t.data||{},this.tagStyle=Object.assign({},l.tagStyle,this.options.tagStyle),this.imgList=t.imgList||[],this.imgList._unloadimgs=0,this.plugins=t.plugins||[],this.attrs=Object.create(null),this.stack=[],this.nodes=[],this.pre=(this.options.containerStyle||"").includes("white-space")&&this.options.containerStyle.includes("pre")?2:0}function o(t){this.handler=t}var l={trustTags:s("a,abbr,ad,audio,b,blockquote,br,code,col,colgroup,dd,del,dl,dt,div,em,fieldset,h1,h2,h3,h4,h5,h6,hr,i,img,ins,label,legend,li,ol,p,q,ruby,rt,source,span,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,title,ul,video"),blockTags:s("address,article,aside,body,caption,center,cite,footer,header,html,nav,pre,section"),ignoreTags:s("area,base,canvas,embed,frame,head,iframe,input,link,map,meta,param,rp,script,source,style,textarea,title,track,wbr"),voidTags:s("area,base,br,col,circle,ellipse,embed,frame,hr,img,input,line,link,meta,param,path,polygon,rect,source,track,use,wbr"),entities:{lt:"<",gt:">",quot:'"',apos:"'",ensp:" ",emsp:" ",nbsp:" ",semi:";",ndash:"–",mdash:"—",middot:"·",lsquo:"‘",rsquo:"’",ldquo:"“",rdquo:"”",bull:"•",hellip:"…",larr:"←",uarr:"↑",rarr:"→",darr:"↓"},tagStyle:{address:"font-style:italic",big:"display:inline;font-size:1.2em",caption:"display:table-caption;text-align:center",center:"text-align:center",cite:"font-style:italic",dd:"margin-left:40px",mark:"background-color:yellow",pre:"font-family:monospace;white-space:pre",s:"text-decoration:line-through",small:"display:inline;font-size:0.8em",strike:"text-decoration:line-through",u:"text-decoration:underline"},svgDict:{animatetransform:"animateTransform",lineargradient:"linearGradient",viewbox:"viewBox",attributename:"attributeName",repeatcount:"repeatCount",repeatdur:"repeatDur",foreignobject:"foreignObject"}},h={},c,d;if(wx.canIUse("getWindowInfo"))c=wx.getWindowInfo().windowWidth,d=wx.getDeviceInfo().system;else{var p=wx.getSystemInfoSync();c=p.windowWidth,d=p.system}var u=s(" ,\r,\n,\t,\f"),f=0;r.prototype.parse=function(t){for(var i=this.plugins.length;i--;)this.plugins[i].onUpdate&&(t=this.plugins[i].onUpdate(t,l)||t);for(new o(this).parse(t);this.stack.length;)this.popNode();return this.nodes.length>50&&a(this.nodes),this.nodes},r.prototype.expose=function(){for(var t=this.stack.length;t--;){var i=this.stack[t];if(i.c||"a"===i.name||"video"===i.name||"audio"===i.name)return;i.c=1}},r.prototype.hook=function(t){for(var i=this.plugins.length;i--;)if(this.plugins[i].onParse&&!1===this.plugins[i].onParse(t,this))return!1;return!0},r.prototype.getUrl=function(t){var i=this.options.domain;return"/"===t[0]?"/"===t[1]?t=(i?i.split("://")[0]:"http")+":"+t:i&&(t=i+t):!i||t.includes("data:")||t.includes("://")||(t=i+"/"+t),t},r.prototype.parseStyle=function(t){var i=t.attrs,e=(this.tagStyle[t.name]||"").split(";").concat((i.style||"").split(";")),s={},n="";i.id&&!this.xml&&(this.options.useAnchor?this.expose():"img"!==t.name&&"a"!==t.name&&"video"!==t.name&&"audio"!==t.name&&(i.id=void 0)),i.width&&(s.width=parseFloat(i.width)+(i.width.includes("%")?"%":"px"),i.width=void 0),i.height&&(s.height=parseFloat(i.height)+(i.height.includes("%")?"%":"px"),i.height=void 0);for(var a=0,r=e.length;a0||h.includes("safe"))n+=";".concat(l,":").concat(h);else if(!s[l]||h.includes("import")||!s[l].includes("import")){if(h.includes("url")){var d=h.indexOf("(")+1;if(d){for(;'"'===h[d]||"'"===h[d]||u[h[d]];)d++;h=h.substr(0,d)+this.getUrl(h.substr(d))}}else h.includes("rpx")&&(h=h.replace(/[0-9.]+\s*rpx/g,function(t){return parseFloat(t)*c/750+"px"}));s[l]=h}}}return t.attrs.style=n,s},r.prototype.onTagName=function(t){this.tagName=this.xml?t:t.toLowerCase(),"svg"===this.tagName&&(this.xml=(this.xml||0)+1,l.ignoreTags.style=void 0)},r.prototype.onAttrName=function(t){t=this.xml?t:t.toLowerCase(),"data-"===t.substr(0,5)?"data-src"!==t||this.attrs.src?"img"===this.tagName||"a"===this.tagName?this.attrName=t:this.attrName=void 0:this.attrName="src":(this.attrName=t,this.attrs[t]="T")},r.prototype.onAttrVal=function(t){var i=this.attrName||"";"style"===i||"href"===i?this.attrs[i]=n(t,!0):i.includes("src")?this.attrs[i]=this.getUrl(n(t,!0)):i&&(this.attrs[i]=t)},r.prototype.onOpenTag=function(t){var i=Object.create(null);i.name=this.tagName,i.attrs=this.attrs,this.attrs=Object.create(null);var e=i.attrs,s=this.stack[this.stack.length-1],n=s?s.children:this.nodes,a=this.xml?t:l.voidTags[i.name];if(h[i.name]&&(e.class=h[i.name]+(e.class?" "+e.class:"")),"embed"===i.name){var r=e.src||"";r.includes(".mp4")||r.includes(".3gp")||r.includes(".m3u8")||(e.type||"").includes("video")?i.name="video":(r.includes(".mp3")||r.includes(".wav")||r.includes(".aac")||r.includes(".m4a")||(e.type||"").includes("audio"))&&(i.name="audio"),e.autostart&&(e.autoplay="T"),e.controls="T"}if("video"!==i.name&&"audio"!==i.name||("video"!==i.name||e.id||(e.id="v"+f++),e.controls||e.autoplay||(e.controls="T"),i.src=[],e.src&&(i.src.push(e.src),e.src=void 0),this.expose()),a){if(!this.hook(i)||l.ignoreTags[i.name])return void("base"!==i.name||this.options.domain?"source"===i.name&&s&&("video"===s.name||"audio"===s.name)&&e.src&&s.src.push(e.src):this.options.domain=e.href);var o=this.parseStyle(i);if("img"===i.name){if(e.src&&(e.src.includes("webp")&&(i.webp="T"),e.src.includes("data:")&&"all"!==this.options.previewImg&&!e["original-src"]&&(e.ignore="T"),!e.ignore||i.webp||e.src.includes("cloud://"))){for(var d=this.stack.length;d--;){var p=this.stack[d];"table"!==p.name||i.webp||e.src.includes("cloud://")||(!o.display||o.display.includes("inline")?i.t="inline-block":i.t=o.display,o.display=void 0);var u=p.attrs.style||"";if(!u.includes("flex:")||u.includes("flex:0")||u.includes("flex: 0")||o.width&&!(parseInt(o.width)>100))if(u.includes("flex")&&"100%"===o.width)for(var g=d+1;g.5?y[b].toUpperCase():y[b];x+=y.substr(b),y=x}}this.imgList.push(y),i.t||(this.imgList._unloadimgs+=1)}"inline"===o.display&&(o.display=""),e.ignore&&(o["max-width"]=o["max-width"]||"100%",e.style+=";-webkit-touch-callout:none"),parseInt(o.width)>c&&(o.height=void 0),isNaN(parseInt(o.width))||(i.w="T"),!isNaN(parseInt(o.height))&&(!o.height.includes("%")||s&&(s.attrs.style||"").includes("height"))&&(i.h="T"),i.w&&i.h&&o["object-fit"]&&("contain"===o["object-fit"]?i.m="aspectFit":"cover"===o["object-fit"]&&(i.m="aspectFill"))}else if("svg"===i.name)return n.push(i),this.stack.push(i),void this.popNode();for(var w in o)o[w]&&(e.style+=";".concat(w,":").concat(o[w].replace(" !important","")));e.style=e.style.substr(1)||void 0}else("pre"===i.name||(e.style||"").includes("white-space")&&e.style.includes("pre"))&&2!==this.pre&&(this.pre=i.pre=1),i.children=[],this.stack.push(i);n.push(i)},r.prototype.onCloseTag=function(t){t=this.xml?t:t.toLowerCase();var i;for(i=this.stack.length;i--&&this.stack[i].name!==t;);if(-1!==i)for(;this.stack.length>i;)this.popNode();else if("p"===t||"br"===t){var e=this.stack.length?this.stack[this.stack.length-1].children:this.nodes;e.push({name:t,attrs:{class:h[t],style:this.tagStyle[t]}})}},r.prototype.popNode=function(){var i=this.stack.pop(),e=i.attrs,s=i.children,n=this.stack[this.stack.length-1],r=n?n.children:this.nodes;if(!this.hook(i)||l.ignoreTags[i.name])return"title"===i.name&&s.length&&"text"===s[0].type&&this.options.setTitle&&wx.setNavigationBarTitle({title:s[0].text}),void r.pop();if(i.pre&&2!==this.pre){this.pre=i.pre=void 0;for(var o=this.stack.length;o--;)this.stack[o].pre&&(this.pre=1)}if("svg"===i.name){if(this.xml>1)return void this.xml--;var h="",d=e.style;return e.style="",e.xmlns="http://www.w3.org/2000/svg",function i(e){if("text"===e.type)return void(h+=e.text);var s=l.svgDict[e.name]||e.name;if("foreignObject"===s){var n,a=t(e.children||[]);try{for(a.s();!(n=a.n()).done;){var r=n.value;if(r.attrs&&!r.attrs.xmlns){r.attrs.xmlns="http://www.w3.org/1999/xhtml";break}}}catch(t){a.e(t)}finally{a.f()}}h+="<"+s;for(var o in e.attrs){var c=e.attrs[o];c&&(h+=" ".concat(l.svgDict[o]||o,'="').concat(c.replace(/"/g,""),'"'))}if(e.children){h+=">";for(var d=0;d"}else h+="/>"}(i),i.name="img",i.attrs={src:"data:image/svg+xml;utf8,"+h.replace(/#/g,"%23"),style:d,ignore:"T"},i.children=void 0,this.xml=!1,void(l.ignoreTags.style=!0)}var p={};if(e.align&&("table"===i.name?"center"===e.align?p["margin-inline-start"]=p["margin-inline-end"]="auto":p.float=e.align:p["text-align"]=e.align,e.align=void 0),e.dir&&(p.direction=e.dir,e.dir=void 0),"font"===i.name&&(e.color&&(p.color=e.color,e.color=void 0),e.face&&(p["font-family"]=e.face,e.face=void 0),e.size)){var u=parseInt(e.size);isNaN(u)||(u<1?u=1:u>7&&(u=7),p["font-size"]=["x-small","small","medium","large","x-large","xx-large","xxx-large"][u-1]),e.size=void 0}if((e.class||"").includes("align-center")&&(p["text-align"]="center"),Object.assign(p,this.parseStyle(i)),"table"!==i.name&&parseInt(p.width)>c&&(p["max-width"]="100%",p["box-sizing"]="border-box"),l.blockTags[i.name])i.name="div";else if(l.trustTags[i.name]||this.xml)if("a"===i.name||"ad"===i.name)this.expose();else if("video"===i.name||"audio"===i.name)(p.height||"").includes("auto")&&(p.height=void 0),i.children=void 0;else if("ul"!==i.name&&"ol"!==i.name||!i.c)if("table"===i.name){var f=parseFloat(e.cellpadding),g=parseFloat(e.cellspacing),m=parseFloat(e.border),v=p["border-color"],y=p["border-style"];if(i.c&&(isNaN(f)&&(f=2),isNaN(g)&&(g=2)),m&&(e.style+=";border:".concat(m,"px ").concat(y||"solid"," ").concat(v||"gray")),i.flag&&i.c){i.flag=void 0,p.display="grid","collapse"===p["border-collapse"]&&(p["border-collapse"]=void 0,g=0),g?(p["grid-gap"]=g+"px",p.padding=g+"px"):m&&(e.style+=";border-left:0;border-top:0");var b=[],x=[],w=[],k={};!function i(e){for(var s=0;s=50&&i.c&&!(p.display||"").includes("flex")&&a(s);for(var E in p)if(p[E]){var G=";".concat(E,":").concat(p[E].replace(" !important",""));_&&(E.includes("flex")&&"flex-direction"!==E||"align-self"===E||E.includes("grid")||"-"===p[E][0]||E.includes("width")&&G.includes("%"))?(i.f+=G,"width"===E&&(e.style+=";width:100%")):e.style+=G}e.style=e.style.substr(1)||void 0},r.prototype.onText=function(t){if(!this.pre){for(var i,e="",s=0,a=t.length;s"===this.content[this.i]||i&&">"===this.content[this.i+1])&&(t&&this.handler[t](this.content.substring(this.start,this.i)),this.i+=i?2:1,this.start=this.i,this.handler.onOpenTag(i),"script"===this.handler.tagName?(this.i=this.content.indexOf("",this.i),-1!==this.i&&(this.i+=2,this.start=this.i),this.state=this.endTag):this.state=this.text,!0)},o.prototype.text=function(){if(this.i=this.content.indexOf("<",this.i),-1===this.i)return void(this.start="a"&&t<="z"||t>="A"&&t<="Z")this.start!==this.i&&this.handler.onText(this.content.substring(this.start,this.i)),this.start=++this.i,this.state=this.tagName;else if("/"===t||"!"===t||"?"===t){this.start!==this.i&&this.handler.onText(this.content.substring(this.start,this.i));var i=this.content[this.i+2];if("/"===t&&(i>="a"&&i<="z"||i>="A"&&i<="Z"))return this.i+=2,this.start=this.i,void(this.state=this.endTag);var e="--\x3e";"!"===t&&"-"===this.content[this.i+2]&&"-"===this.content[this.i+3]||(e=">"),this.i=this.content.indexOf(e,this.i),-1!==this.i&&(this.i+=e.length,this.start=this.i)}else this.i++},o.prototype.tagName=function(){if(u[this.content[this.i]]){for(this.handler.onTagName(this.content.substring(this.start,this.i));u[this.content[++this.i]];);this.i"===t||"/"===t){if(this.handler.onCloseTag(this.content.substring(this.start,this.i)),">"!==t&&(this.i=this.content.indexOf(">",this.i),-1===this.i))return;this.start=++this.i,this.state=this.text}else this.i++},module.exports=r;
\ No newline at end of file
diff --git a/apps/miniprogram/src/pages/article/detail/index.config.ts b/apps/miniprogram/src/pages/article/detail/index.config.ts
new file mode 100644
index 0000000..2d657be
--- /dev/null
+++ b/apps/miniprogram/src/pages/article/detail/index.config.ts
@@ -0,0 +1,6 @@
+export default definePageConfig({
+ navigationBarTitleText: '文章详情',
+ usingComponents: {
+ 'mp-html': '../../../native-components/mp-html/index',
+ },
+});
diff --git a/apps/miniprogram/src/pages/article/detail/index.scss b/apps/miniprogram/src/pages/article/detail/index.scss
index 31027e7..6f5c4e8 100644
--- a/apps/miniprogram/src/pages/article/detail/index.scss
+++ b/apps/miniprogram/src/pages/article/detail/index.scss
@@ -1,19 +1,21 @@
@import '../../../styles/variables.scss';
-// 文章详情页 — 对齐原型 docs/design/mp-04-article-report.html → ArticleDetail
+// 文章详情页 — 阅读优化排版
.article-detail-page {
- padding-bottom: 80px;
+ padding-bottom: 100px;
+ background: $card;
}
.article-title {
font-family: Georgia, 'Times New Roman', serif;
- font-size: var(--tk-font-h2);
+ font-size: var(--tk-font-h1);
font-weight: 700;
color: $tx;
display: block;
- line-height: 1.4;
- margin-bottom: var(--tk-gap-sm);
+ line-height: 1.35;
+ margin-bottom: var(--tk-gap-md);
+ letter-spacing: 0.5px;
}
.article-meta {
@@ -21,36 +23,38 @@
gap: var(--tk-gap-md);
font-size: var(--tk-font-cap);
color: $tx3;
- margin-bottom: var(--tk-gap-md);
+ margin-bottom: var(--tk-gap-lg);
+ align-items: center;
+}
+
+.meta-item {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.meta-icon {
+ font-size: 13px;
+ line-height: 1;
}
.article-divider {
height: 1px;
- background: $bd-l;
+ background: linear-gradient(90deg, $bd-l, $bd, $bd-l);
margin-bottom: var(--tk-section-gap);
}
.article-body {
- font-size: 15px;
+ // RichText 内部样式由 formatArticleHtml 内联注入
+ // 这里只控制容器间距
+ font-size: var(--tk-font-body);
color: $tx2;
line-height: 1.8;
- // RichText 内部样式
- h1, h2, h3 {
- font-weight: bold;
+ // 兜底:万一内联样式未生效的标签
+ h1, h2, h3, h4, h5, h6 {
color: $tx;
- margin: var(--tk-gap-lg) 0 var(--tk-gap-sm);
- }
-
- p {
- margin-bottom: var(--tk-gap-md);
- text-indent: 2em;
- }
-
- img {
- max-width: 100%;
- border-radius: $r-sm;
- margin: var(--tk-gap-sm) 0;
+ line-height: 1.4;
}
}
@@ -59,31 +63,40 @@
bottom: 0;
left: 0;
right: 0;
- height: 60px;
+ height: 64px;
background: $card;
border-top: 1px solid $bd-l;
display: flex;
align-items: center;
justify-content: center;
- gap: 32px;
+ gap: 48px;
padding: 0 var(--tk-page-padding);
z-index: 10;
+ // 安全区适配
+ padding-bottom: env(safe-area-inset-bottom);
+ box-shadow: 0 -2px 12px rgba(45, 42, 38, 0.06);
}
.article-action-btn {
display: flex;
flex-direction: column;
align-items: center;
- gap: 2px;
+ gap: 4px;
+ min-width: $touch-min;
+ min-height: $touch-min;
+ justify-content: center;
+ border-radius: $r-sm;
+ transition: background var(--tk-duration-fast);
&:active {
+ background: $surface-alt;
opacity: var(--tk-touch-feedback-opacity);
}
}
.article-action-icon {
- font-size: 20px;
- color: $tx3;
+ font-size: 22px;
+ color: $tx2;
line-height: 1;
}
@@ -105,3 +118,29 @@
font-size: var(--tk-font-body);
color: $tx3;
}
+
+// ─── 关怀模式覆盖 ───
+.elder-mode {
+ .article-title {
+ font-size: var(--tk-font-h1);
+ line-height: 1.3;
+ }
+
+ .article-body {
+ font-size: var(--tk-font-body);
+ line-height: 2;
+ }
+
+ .article-bottom-bar {
+ height: 72px;
+ gap: 56px;
+ }
+
+ .article-action-icon {
+ font-size: 26px;
+ }
+
+ .article-action-text {
+ font-size: var(--tk-font-body-sm);
+ }
+}
diff --git a/apps/miniprogram/src/pages/article/detail/index.tsx b/apps/miniprogram/src/pages/article/detail/index.tsx
index e7fbf9b..92f23f1 100644
--- a/apps/miniprogram/src/pages/article/detail/index.tsx
+++ b/apps/miniprogram/src/pages/article/detail/index.tsx
@@ -1,13 +1,13 @@
import { useState, useCallback } from 'react';
-import { View, Text, RichText } from '@tarojs/components';
+import { View, Text } from '@tarojs/components';
import Taro, { useRouter, useShareAppMessage } from '@tarojs/taro';
import { usePageData } from '@/hooks/usePageData';
import { getArticleDetail, getPublicArticleDetail, Article } from '../../../services/article';
import { trackEvent } from '@/services/analytics';
-import { sanitizeHtml } from '@/utils/sanitize-html';
import { useElderClass } from '../../../hooks/useElderClass';
import { useAuthStore } from '../../../stores/auth';
import PageShell from '@/components/ui/PageShell';
+import RichArticle from '@/components/RichArticle';
import './index.scss';
export default function ArticleDetail() {
@@ -77,14 +77,24 @@ export default function ArticleDetail() {
{article.title}
- {article.author && {article.author}}
- {article.published_at && {article.published_at.slice(0, 10)}}
+ {article.author && (
+
+ ✍
+ {article.author}
+
+ )}
+ {article.published_at && (
+
+ 📅
+ {article.published_at.slice(0, 10)}
+
+ )}
-
+
diff --git a/apps/miniprogram/src/types/mp-html.d.ts b/apps/miniprogram/src/types/mp-html.d.ts
new file mode 100644
index 0000000..ac4aef8
--- /dev/null
+++ b/apps/miniprogram/src/types/mp-html.d.ts
@@ -0,0 +1,15 @@
+declare namespace JSX {
+ interface IntrinsicElements {
+ 'mp-html': {
+ content?: string;
+ 'lazy-load'?: boolean;
+ selectable?: boolean;
+ 'show-img-menu'?: boolean;
+ domain?: string;
+ 'tag-style'?: string;
+ 'link-style'?: string;
+ 'container-style'?: string;
+ onReady?: () => void;
+ };
+ }
+}
diff --git a/apps/miniprogram/src/utils/sanitize-html.ts b/apps/miniprogram/src/utils/sanitize-html.ts
index 6f31955..dc61b81 100644
--- a/apps/miniprogram/src/utils/sanitize-html.ts
+++ b/apps/miniprogram/src/utils/sanitize-html.ts
@@ -11,11 +11,12 @@ const ALLOWED_TAGS = new Set([
]);
const ALLOWED_ATTRS: Record> = {
- '*': new Set(['class']),
+ '*': new Set(['class', 'style', 'data-w-e-type']),
a: new Set(['href', 'title']),
img: new Set(['src', 'alt', 'width', 'height']),
td: new Set(['colspan', 'rowspan']),
th: new Set(['colspan', 'rowspan']),
+ span: new Set(['style']),
};
const URL_ATTRS = new Set(['href', 'src']);