feat: 电子药箱小程序 - 4Tab药品管理
- 电子药箱: 药品库存管理 + 手动录入 + 今日用药打卡 - 药品百科: 搜索 + 分类筛选 + 20种药品静态数据 - 惠教中心: 4条药品使用指南课程 - 我的: 家庭成员信息管理 - 自定义TabBar + navigation-bar组件 - SVG药品分类插图
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
// html-to-wx-nodes.ts
|
||||
declare global {
|
||||
interface DOMParser {
|
||||
new (): DOMParser;
|
||||
parseFromString(source: string, mimeType: string): Document;
|
||||
}
|
||||
|
||||
interface Document {
|
||||
body: HTMLElement;
|
||||
}
|
||||
|
||||
interface HTMLElement extends Element {
|
||||
innerHTML: string;
|
||||
}
|
||||
|
||||
interface Element {
|
||||
tagName: string;
|
||||
attributes: NamedNodeMap;
|
||||
childNodes: ChildNode[];
|
||||
}
|
||||
|
||||
interface ChildNode {
|
||||
nodeType: number;
|
||||
textContent: string | null;
|
||||
}
|
||||
|
||||
interface NamedNodeMap {
|
||||
length: number;
|
||||
item(index: number): Attr | null;
|
||||
}
|
||||
|
||||
interface Attr {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const Node: {
|
||||
TEXT_NODE: number;
|
||||
ELEMENT_NODE: number;
|
||||
};
|
||||
|
||||
var DOMParser: {
|
||||
prototype: DOMParser;
|
||||
new (): DOMParser;
|
||||
};
|
||||
}
|
||||
|
||||
type WxNode = {
|
||||
name?: string; // 元素节点:标签名,如 'div','img'
|
||||
attrs?: Record<string, string>; // 属性集合
|
||||
children?: WxNode[]; // 子节点(元素或文本)
|
||||
type?: "text"; // 文本节点标记
|
||||
text?: string; // 文本节点内容
|
||||
};
|
||||
|
||||
function decodeHtmlEntities(str: string) {
|
||||
return str.replace(/<|>|&|"|'/g, (s) => {
|
||||
switch (s) {
|
||||
case "<":
|
||||
return "<";
|
||||
case ">":
|
||||
return ">";
|
||||
case "&":
|
||||
return "&";
|
||||
case """:
|
||||
return '"';
|
||||
case "'":
|
||||
return "'";
|
||||
default:
|
||||
return s;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function elementToNode(el: Element): WxNode {
|
||||
const node: WxNode = {
|
||||
name: el.tagName.toLowerCase(),
|
||||
attrs: {},
|
||||
children: [],
|
||||
};
|
||||
|
||||
// copy attributes
|
||||
for (let i = 0; i < el.attributes.length; i++) {
|
||||
const a = el.attributes.item(i)!;
|
||||
node.attrs![a.name] = a.value;
|
||||
}
|
||||
|
||||
// map special: if data-align present, try to map to style (optional)
|
||||
if (node.attrs!["data-align"] && !node.attrs!["style"]) {
|
||||
const align = node.attrs!["data-align"];
|
||||
if (node.name === "img") {
|
||||
if (align === "center") {
|
||||
node.attrs!["style"] = "display:block;margin:0 auto;";
|
||||
} else if (align === "left") {
|
||||
node.attrs!["style"] = "float:left;margin-right:8px;";
|
||||
} else if (align === "right") {
|
||||
node.attrs!["style"] = "float:right;margin-left:8px;";
|
||||
}
|
||||
} else {
|
||||
// for block elements like div, p
|
||||
node.attrs!["style"] = `text-align:${align};`;
|
||||
}
|
||||
}
|
||||
|
||||
// children: process childNodes
|
||||
el.childNodes.forEach((child: ChildNode) => {
|
||||
if (child.nodeType === Node.TEXT_NODE) {
|
||||
const txt = (child.textContent || "").trim();
|
||||
if (txt.length > 0) {
|
||||
node.children!.push({ type: "text", text: decodeHtmlEntities(txt) });
|
||||
}
|
||||
} else if (child.nodeType === Node.ELEMENT_NODE) {
|
||||
node.children!.push(elementToNode(child as Element & ChildNode));
|
||||
}
|
||||
});
|
||||
|
||||
// if no children for img, remove children prop (not required)
|
||||
if (node.name === "img") {
|
||||
delete node.children;
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
export function htmlToWxNodes(html: string): WxNode[] {
|
||||
html = (html || "").trim();
|
||||
if (!html) return [];
|
||||
|
||||
// try DOMParser (browser / devtools)
|
||||
if (typeof DOMParser !== "undefined") {
|
||||
try {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, "text/html");
|
||||
|
||||
const body = doc.body;
|
||||
const nodes: WxNode[] = [];
|
||||
body.childNodes.forEach((child) => {
|
||||
if (child.nodeType === Node.TEXT_NODE) {
|
||||
const txt = (child.textContent || "").trim();
|
||||
if (txt) nodes.push({ type: "text", text: decodeHtmlEntities(txt) });
|
||||
} else if (child.nodeType === Node.ELEMENT_NODE) {
|
||||
nodes.push(elementToNode(child as Element & ChildNode));
|
||||
}
|
||||
});
|
||||
return nodes;
|
||||
} catch (e) {
|
||||
// fallthrough to fallback parser
|
||||
console.warn("DOMParser failed, fallback to simple parse", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: very small parser for simple HTML (img, div, p, plain text)
|
||||
const fallbackNodes: WxNode[] = [];
|
||||
const imgRegex = /<img\b([^>]+?)\/?>/gi;
|
||||
let lastIndex = 0;
|
||||
let m;
|
||||
while ((m = imgRegex.exec(html)) !== null) {
|
||||
// push text before this img as text node
|
||||
if (m.index > lastIndex) {
|
||||
const txt = html
|
||||
.slice(lastIndex, m.index)
|
||||
.replace(/<[^>]+>/g, "")
|
||||
.trim();
|
||||
if (txt)
|
||||
fallbackNodes.push({ type: "text", text: decodeHtmlEntities(txt) });
|
||||
}
|
||||
const attrsStr = m[1];
|
||||
const attrs: Record<string, string> = {};
|
||||
attrsStr.replace(
|
||||
/([^\s=]+)(?:="([^"]*)")?/g,
|
||||
(_: string, k: string, v: string) => {
|
||||
attrs[k] = v || "";
|
||||
return "";
|
||||
}
|
||||
);
|
||||
// normalize src, alt
|
||||
const node: WxNode = {
|
||||
name: "img",
|
||||
attrs: {
|
||||
src: attrs.src || attrs["data-src"] || "",
|
||||
alt: attrs.alt || "",
|
||||
},
|
||||
};
|
||||
if (attrs["data-align"]) {
|
||||
const al = attrs["data-align"];
|
||||
if (al === "center")
|
||||
node.attrs!["style"] =
|
||||
"display:block;margin:0 auto;max-width:100%;height:auto;";
|
||||
if (al === "left")
|
||||
node.attrs!["style"] =
|
||||
"float:left;margin-right:8px;max-width:100%;height:auto;";
|
||||
if (al === "right")
|
||||
node.attrs!["style"] =
|
||||
"float:right;margin-left:8px;max-width:100%;height:auto;";
|
||||
}
|
||||
fallbackNodes.push(node);
|
||||
lastIndex = imgRegex.lastIndex;
|
||||
}
|
||||
// trailing text
|
||||
if (lastIndex < html.length) {
|
||||
const trailing = html
|
||||
.slice(lastIndex)
|
||||
.replace(/<[^>]+>/g, "")
|
||||
.trim();
|
||||
if (trailing)
|
||||
fallbackNodes.push({ type: "text", text: decodeHtmlEntities(trailing) });
|
||||
}
|
||||
return fallbackNodes;
|
||||
}
|
||||
Reference in New Issue
Block a user