'cbook'
This commit is contained in:
@@ -60,8 +60,8 @@ const config = {
|
||||
chain.module
|
||||
.rule("markdown")
|
||||
.test(/\.md$/)
|
||||
.use("raw-loader")
|
||||
.loader("raw-loader")
|
||||
.use("md-chunk-loader")
|
||||
.loader(path.resolve(__dirname, "..", "src", "loaders", "md-chunk-loader.js"))
|
||||
.end();
|
||||
|
||||
chain.module
|
||||
|
||||
33
src/loaders/md-chunk-loader.js
Normal file
33
src/loaders/md-chunk-loader.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const { marked } = require("marked");
|
||||
|
||||
module.exports = function mdChunkLoader(source) {
|
||||
this.cacheable && this.cacheable();
|
||||
|
||||
const html = marked.parse(source, { breaks: true, gfm: true });
|
||||
|
||||
const lazyHtml = html.replace(/<img /g, '<img loading="lazy" decoding="async" ');
|
||||
|
||||
let rawChunks = lazyHtml.split(/(?=<h1[\s>])/i).filter(c => c.trim());
|
||||
if (rawChunks.length <= 1) {
|
||||
rawChunks = lazyHtml.split(/(?=<h2[\s>])/i).filter(c => c.trim());
|
||||
}
|
||||
|
||||
const headers = [];
|
||||
const headerRegex = /<h1[^>]*>(.*?)<\/h1>/gi;
|
||||
let m;
|
||||
while ((m = headerRegex.exec(lazyHtml)) !== null) {
|
||||
headers.push(m[1].replace(/<[^>]+>/g, ""));
|
||||
}
|
||||
|
||||
const chunks = rawChunks.map(chunkHtml => {
|
||||
const imgCount = (chunkHtml.match(/<img /g) || []).length;
|
||||
const textLen = chunkHtml.replace(/<[^>]+>/g, "").length;
|
||||
const estimatedHeight = Math.max(300, Math.round(textLen * 0.6 + imgCount * 350));
|
||||
return { html: chunkHtml, estimatedHeight };
|
||||
});
|
||||
|
||||
return [
|
||||
"export var chunks = " + JSON.stringify(chunks) + ";",
|
||||
"export var headers = " + JSON.stringify(headers) + ";",
|
||||
].join("\n");
|
||||
};
|
||||
@@ -1,5 +1,17 @@
|
||||
@import "github-markdown-css/github-markdown.css";
|
||||
|
||||
// ===== 阅读进度条 =====
|
||||
.reading-progress {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
|
||||
z-index: 201;
|
||||
transition: width 0.15s linear;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
// ===== 顶部导航栏 =====
|
||||
.book_header_div {
|
||||
width: 100%;
|
||||
@@ -13,9 +25,10 @@
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
height: 88px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
@@ -71,11 +84,102 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 段落渐入动画 =====
|
||||
@keyframes chunkAppear {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.chunk-enter {
|
||||
animation: chunkAppear 0.5s ease-out both;
|
||||
}
|
||||
|
||||
// ===== 骨架屏占位 =====
|
||||
@keyframes shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
.chunk-placeholder {
|
||||
background: linear-gradient(90deg, #f6f7f8 25%, #edeef1 37%, #f6f7f8 63%);
|
||||
background-size: 400% 100%;
|
||||
animation: shimmer 1.8s ease infinite;
|
||||
border-radius: 12px;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
// ===== 加载状态 =====
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(16px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
// ===== 回到顶部 =====
|
||||
.back-to-top {
|
||||
position: fixed;
|
||||
bottom: 120px;
|
||||
right: 30px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
z-index: 50;
|
||||
animation: fadeInUp 0.3s ease;
|
||||
color: #4facfe;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.9);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 内容区域 =====
|
||||
.Book {
|
||||
padding: 30px;
|
||||
max-width: 100%;
|
||||
|
||||
.markdown-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 500px;
|
||||
gap: 28px;
|
||||
|
||||
.loading-spinner {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border: 5px solid #f0f0f0;
|
||||
border-top-color: #4facfe;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #8c8c8c;
|
||||
font-size: 28px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
font-size: 32px !important;
|
||||
line-height: 1.85 !important;
|
||||
@@ -87,17 +191,27 @@
|
||||
font-size: 48px !important;
|
||||
font-weight: 800;
|
||||
color: #1a1a2e;
|
||||
margin: 48px 0 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 2px solid #e8e8e8;
|
||||
margin: 56px 0 20px;
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
line-height: 1.3;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 80px;
|
||||
height: 6px;
|
||||
background: linear-gradient(90deg, #4facfe, #00f2fe);
|
||||
border-radius: 3px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 42px !important;
|
||||
font-weight: 700;
|
||||
color: #2c3e50;
|
||||
margin: 40px 0 20px;
|
||||
margin: 44px 0 18px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
line-height: 1.3;
|
||||
@@ -107,14 +221,14 @@
|
||||
font-size: 36px !important;
|
||||
font-weight: 600;
|
||||
color: #34495e;
|
||||
margin: 32px 0 16px;
|
||||
margin: 36px 0 14px;
|
||||
}
|
||||
|
||||
h4, h5, h6 {
|
||||
font-size: 32px !important;
|
||||
font-weight: 600;
|
||||
color: #4a5568;
|
||||
margin: 24px 0 12px;
|
||||
margin: 28px 0 12px;
|
||||
}
|
||||
|
||||
p {
|
||||
@@ -124,13 +238,25 @@
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 24px 0;
|
||||
padding: 20px 24px;
|
||||
border-left: 6px solid #4facfe;
|
||||
background: #f8fbff;
|
||||
position: relative;
|
||||
margin: 28px 0;
|
||||
padding: 20px 24px 20px 30px;
|
||||
border-left: none;
|
||||
background: linear-gradient(135deg, #f8fbff 0%, #f0f7ff 100%);
|
||||
border-radius: 0 12px 12px 0;
|
||||
color: #4a5568;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 6px;
|
||||
background: linear-gradient(180deg, #4facfe, #00f2fe);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 8px;
|
||||
&:last-child { margin-bottom: 0; }
|
||||
@@ -138,13 +264,25 @@
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 24px 0;
|
||||
padding: 24px;
|
||||
position: relative;
|
||||
margin: 28px 0;
|
||||
padding: 28px 24px 24px;
|
||||
background: #1e1e2e;
|
||||
border-radius: 12px;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, #4facfe, #00f2fe);
|
||||
border-radius: 12px 12px 0 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "SF Mono", "Fira Code", Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 26px !important;
|
||||
@@ -192,15 +330,16 @@
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
margin: 24px 0;
|
||||
margin: 28px 0;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
border-radius: 8px;
|
||||
|
||||
th {
|
||||
background: #f8fafc;
|
||||
background: linear-gradient(135deg, #f8fafc, #edf2f7);
|
||||
font-weight: 600;
|
||||
padding: 16px 20px;
|
||||
text-align: left;
|
||||
@@ -216,20 +355,30 @@
|
||||
}
|
||||
|
||||
tr:nth-child(even) { background: #f8fafc; }
|
||||
tr { transition: background 0.15s ease; }
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 40px 0;
|
||||
margin: 48px 0;
|
||||
border: none;
|
||||
border-top: 2px solid #f0f0f0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, #e2e8f0, transparent);
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
margin: 24px auto;
|
||||
margin: 28px auto;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
transition: opacity 0.6s ease, transform 0.6s ease, box-shadow 0.3s ease;
|
||||
|
||||
&.loaded {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -237,8 +386,12 @@
|
||||
text-decoration: none;
|
||||
font-size: inherit !important;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border-color 0.2s;
|
||||
&:hover { border-bottom-color: #4facfe; }
|
||||
transition: border-color 0.2s, color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #2b8de3;
|
||||
border-bottom-color: #4facfe;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
@@ -258,6 +411,12 @@
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
transition: background 0.2s;
|
||||
|
||||
&[open] {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
summary {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
@@ -266,15 +425,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
color: #8c8c8c;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
// 二维码区域
|
||||
.qrcode-section {
|
||||
display: flex;
|
||||
@@ -332,8 +482,24 @@
|
||||
|
||||
/* stylelint-disable */
|
||||
// ===== Tablet/Desktop: 768px+ =====
|
||||
// 使用 PX (大写) 防止 Taro postcss-pxtransform 转换
|
||||
@media (min-width: 768PX) {
|
||||
.reading-progress {
|
||||
height: 3PX;
|
||||
}
|
||||
|
||||
.back-to-top {
|
||||
width: 44PX;
|
||||
height: 44PX;
|
||||
bottom: 40PX;
|
||||
right: 40PX;
|
||||
box-shadow: 0 2PX 12PX rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2PX);
|
||||
box-shadow: 0 4PX 16PX rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.book_header {
|
||||
height: 56PX;
|
||||
|
||||
@@ -369,6 +535,17 @@
|
||||
.Book {
|
||||
padding: 30PX 60PX;
|
||||
|
||||
.markdown-loading {
|
||||
.loading-spinner {
|
||||
width: 36PX;
|
||||
height: 36PX;
|
||||
border-width: 3PX;
|
||||
}
|
||||
.loading-text {
|
||||
font-size: 15PX;
|
||||
}
|
||||
}
|
||||
|
||||
.book_AtDrawer .at-drawer__content {
|
||||
padding-top: 56PX;
|
||||
width: 300PX;
|
||||
@@ -399,14 +576,20 @@
|
||||
|
||||
h1 {
|
||||
font-size: 28PX !important;
|
||||
margin: 32PX 0 16PX;
|
||||
padding-bottom: 10PX;
|
||||
border-bottom-width: 2PX;
|
||||
margin: 36PX 0 14PX;
|
||||
|
||||
&::after {
|
||||
width: 50PX;
|
||||
height: 3PX;
|
||||
margin-top: 10PX;
|
||||
border-radius: 2PX;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 23PX !important;
|
||||
margin: 26PX 0 12PX;
|
||||
margin: 28PX 0 12PX;
|
||||
padding-bottom: 8PX;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@@ -428,10 +611,15 @@
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 18PX;
|
||||
padding: 22PX 18PX 18PX;
|
||||
border-radius: 8PX;
|
||||
margin: 20PX 0;
|
||||
|
||||
&::before {
|
||||
height: 3PX;
|
||||
border-radius: 8PX 8PX 0 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 14PX !important;
|
||||
}
|
||||
@@ -449,17 +637,21 @@
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding: 14PX 18PX;
|
||||
border-left-width: 4PX;
|
||||
padding: 14PX 18PX 14PX 22PX;
|
||||
margin: 18PX 0;
|
||||
border-radius: 0 8PX 8PX 0;
|
||||
|
||||
&::before {
|
||||
width: 4PX;
|
||||
border-radius: 2PX;
|
||||
}
|
||||
}
|
||||
|
||||
ul, ol { padding-left: 24PX; }
|
||||
img { border-radius: 8PX; margin: 20PX auto; }
|
||||
hr { margin: 24PX 0; border-top-width: 2PX; }
|
||||
hr { margin: 28PX 0; }
|
||||
|
||||
a { font-size: inherit !important; border-bottom-width: 1PX; }
|
||||
a { font-size: inherit !important; }
|
||||
strong { font-size: inherit !important; }
|
||||
}
|
||||
|
||||
@@ -494,13 +686,18 @@
|
||||
li { font-size: 17PX !important; }
|
||||
|
||||
pre {
|
||||
padding: 18PX 22PX;
|
||||
padding: 22PX 22PX 18PX;
|
||||
code { font-size: 14PX !important; }
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: 10PX;
|
||||
max-width: 90%;
|
||||
|
||||
&.loaded:hover {
|
||||
box-shadow: 0 8PX 30PX rgba(0, 0, 0, 0.12);
|
||||
transform: translateY(-2PX);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,37 +3,14 @@ import { View, Image } from "@tarojs/components";
|
||||
import Taro, { useRouter } from "@tarojs/taro";
|
||||
import "./index.scss";
|
||||
import { AtButton, AtDrawer, AtIcon } from "taro-ui";
|
||||
import React, { useEffect, useState, useRef, useMemo, useCallback } from "react";
|
||||
import { marked } from "marked";
|
||||
import infoText from "./info.md";
|
||||
import bookText from "./book.md";
|
||||
import React, { useEffect, useState, useRef, useCallback } from "react";
|
||||
import { chunks as infoChunks, headers as infoHeaders } from "./info.md";
|
||||
import hackrobot from "../../images/hackrobot.jpg";
|
||||
|
||||
const PB_URL = "https://pocketbase.hackrobot.cn";
|
||||
const pb = new PocketBase(PB_URL);
|
||||
|
||||
marked.setOptions({ breaks: true, gfm: true });
|
||||
|
||||
function addLazyLoadToImages(html) {
|
||||
return html.replace(/<img /g, '<img loading="lazy" decoding="async" ');
|
||||
}
|
||||
|
||||
function splitIntoChunks(html) {
|
||||
const parts = html.split(/(?=<h1[\s>])/i);
|
||||
return parts.filter(p => p.trim().length > 0);
|
||||
}
|
||||
|
||||
function extractHeaders(html) {
|
||||
const regex = /<h1[^>]*>(.*?)<\/h1>/gi;
|
||||
const results = [];
|
||||
let match;
|
||||
while ((match = regex.exec(html)) !== null) {
|
||||
results.push(match[1].replace(/<[^>]+>/g, ""));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
const LazyChunk = React.memo(({ html, forceVisible }) => {
|
||||
const LazyChunk = React.memo(({ html, estimatedHeight, forceVisible }) => {
|
||||
const [visible, setVisible] = useState(forceVisible);
|
||||
const ref = useRef(null);
|
||||
|
||||
@@ -49,28 +26,29 @@ const LazyChunk = React.memo(({ html, forceVisible }) => {
|
||||
observer.disconnect();
|
||||
}
|
||||
},
|
||||
{ rootMargin: "800px 0px" }
|
||||
{ rootMargin: "1200px 0px" }
|
||||
);
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, [forceVisible, visible]);
|
||||
|
||||
if (visible) {
|
||||
return <div dangerouslySetInnerHTML={{ __html: html }} />;
|
||||
return <div className="chunk-enter" dangerouslySetInnerHTML={{ __html: html }} />;
|
||||
}
|
||||
return <div ref={ref} style={{ minHeight: "200px" }} />;
|
||||
return <div ref={ref} className="chunk-placeholder" style={{ minHeight: estimatedHeight }} />;
|
||||
});
|
||||
|
||||
const Book = () => {
|
||||
const [isWeChat, setisWeChat] = useState(false);
|
||||
const [show, setshow] = useState(false);
|
||||
const [headers, setHeaders] = useState([]);
|
||||
const contentRef = useRef(null);
|
||||
const [paidType, setPaidType] = useState(null);
|
||||
const [userId, setUserId] = useState(Taro.getStorageSync("userId") || null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [contentReady, setContentReady] = useState(false);
|
||||
const [forceShowAll, setForceShowAll] = useState(false);
|
||||
const [readProgress, setReadProgress] = useState(0);
|
||||
const [showBackTop, setShowBackTop] = useState(false);
|
||||
const [bookData, setBookData] = useState(null);
|
||||
const router = useRouter();
|
||||
const [vipbook, setVipbook] = useState(false);
|
||||
|
||||
@@ -126,9 +104,7 @@ const Book = () => {
|
||||
|
||||
const isWeChatFun = () => {
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
const result = ua.includes("micromessenger");
|
||||
setisWeChat(result);
|
||||
return result;
|
||||
setisWeChat(ua.includes("micromessenger"));
|
||||
};
|
||||
|
||||
const getOrCreateUserId = () => {
|
||||
@@ -162,18 +138,70 @@ const Book = () => {
|
||||
init();
|
||||
}, []);
|
||||
|
||||
const isPaid = paidType === "book" || vipbook;
|
||||
|
||||
useEffect(() => {
|
||||
if (isPaid && !bookData) {
|
||||
import("./book.md").then(mod => {
|
||||
setBookData({ chunks: mod.chunks, headers: mod.headers });
|
||||
});
|
||||
}
|
||||
}, [isPaid, bookData]);
|
||||
|
||||
const activeChunks = isPaid && bookData ? bookData.chunks : infoChunks;
|
||||
const activeHeaders = isPaid && bookData ? bookData.headers : infoHeaders;
|
||||
const contentLoading = loading || (isPaid && !bookData);
|
||||
|
||||
useEffect(() => {
|
||||
let ticking = false;
|
||||
const onScroll = () => {
|
||||
if (ticking) return;
|
||||
ticking = true;
|
||||
requestAnimationFrame(() => {
|
||||
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
||||
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
|
||||
setReadProgress(docHeight > 0 ? Math.min(100, (scrollTop / docHeight) * 100) : 0);
|
||||
setShowBackTop(scrollTop > 600);
|
||||
ticking = false;
|
||||
});
|
||||
};
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", onScroll);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const el = contentRef.current;
|
||||
if (!el || contentLoading) return;
|
||||
const onImgLoad = (e) => {
|
||||
if (e.target.tagName === "IMG") e.target.classList.add("loaded");
|
||||
};
|
||||
el.addEventListener("load", onImgLoad, true);
|
||||
const checkCached = () => {
|
||||
el.querySelectorAll("img").forEach(img => {
|
||||
if (img.complete) img.classList.add("loaded");
|
||||
});
|
||||
};
|
||||
const mo = new MutationObserver(checkCached);
|
||||
mo.observe(el, { childList: true, subtree: true });
|
||||
checkCached();
|
||||
return () => {
|
||||
el.removeEventListener("load", onImgLoad, true);
|
||||
mo.disconnect();
|
||||
};
|
||||
}, [contentLoading]);
|
||||
|
||||
const handleJump = useCallback((text) => {
|
||||
setForceShowAll(true);
|
||||
setTimeout(() => {
|
||||
if (!contentRef.current) return;
|
||||
const h1Elements = contentRef.current.querySelectorAll("h1");
|
||||
for (let h1 of h1Elements) {
|
||||
const h1s = contentRef.current.querySelectorAll("h1");
|
||||
for (let h1 of h1s) {
|
||||
if (h1.textContent.trim() === text.trim()) {
|
||||
h1.scrollIntoView({ behavior: "smooth" });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, 150);
|
||||
}, 200);
|
||||
}, []);
|
||||
|
||||
const submitPayForm = (xorpayUrl, xorpayParams) => {
|
||||
@@ -211,45 +239,35 @@ const Book = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const isPaid = paidType === "book" || vipbook;
|
||||
|
||||
const markdownSource = isPaid ? bookText : infoText;
|
||||
const parsedHtml = useMemo(() => addLazyLoadToImages(marked(markdownSource)), [markdownSource]);
|
||||
const chunks = useMemo(() => splitIntoChunks(parsedHtml), [parsedHtml]);
|
||||
const headerList = useMemo(() => extractHeaders(parsedHtml), [parsedHtml]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) return;
|
||||
setHeaders(headerList);
|
||||
const raf = requestAnimationFrame(() => {
|
||||
setContentReady(true);
|
||||
});
|
||||
return () => cancelAnimationFrame(raf);
|
||||
}, [loading, headerList]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<View className="reading-progress" style={{ width: `${readProgress}%` }} />
|
||||
|
||||
<View className="book_header_div" />
|
||||
<View className="book_header">
|
||||
<View className="book_header_left" onClick={() => Taro.redirectTo({ url: "/" })}>
|
||||
<AtIcon value="chevron-left" />
|
||||
</View>
|
||||
<View className="book_header_middle">数字游民</View>
|
||||
<View className={`book_header_right ${show ? 'active' : ''}`} onClick={() => setshow(!show)}>
|
||||
<View className={`book_header_right ${show ? "active" : ""}`} onClick={() => setshow(!show)}>
|
||||
<AtIcon value="bullet-list" />
|
||||
<View className="book_header_right_text pc-only">目录</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="Book">
|
||||
{!contentReady ? (
|
||||
<View className="markdown-loading">加载中...</View>
|
||||
{contentLoading ? (
|
||||
<View className="markdown-loading">
|
||||
<View className="loading-spinner" />
|
||||
<View className="loading-text">正在加载...</View>
|
||||
</View>
|
||||
) : (
|
||||
<View ref={contentRef} className="markdown-body">
|
||||
{chunks.map((chunk, i) => (
|
||||
{activeChunks.map((chunk, i) => (
|
||||
<LazyChunk
|
||||
key={i}
|
||||
html={chunk}
|
||||
key={`${isPaid}-${i}`}
|
||||
html={chunk.html}
|
||||
estimatedHeight={chunk.estimatedHeight}
|
||||
forceVisible={forceShowAll || i === 0}
|
||||
/>
|
||||
))}
|
||||
@@ -260,8 +278,8 @@ const Book = () => {
|
||||
className="book_AtDrawer"
|
||||
show={show}
|
||||
mask
|
||||
items={headers}
|
||||
onItemClick={(index) => handleJump(headers[index])}
|
||||
items={activeHeaders}
|
||||
onItemClick={(index) => handleJump(activeHeaders[index])}
|
||||
onClose={() => setshow(false)}
|
||||
/>
|
||||
|
||||
@@ -293,6 +311,15 @@ const Book = () => {
|
||||
</AtButton>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{showBackTop && (
|
||||
<View
|
||||
className="back-to-top"
|
||||
onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}
|
||||
>
|
||||
<AtIcon value="chevron-up" size="20" />
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,47 +1,16 @@
|
||||
// @ts-nocheck
|
||||
import { Component } from "react";
|
||||
import { View, Text, Image } from "@tarojs/components";
|
||||
import { View } from "@tarojs/components";
|
||||
import "./index.scss";
|
||||
import { AtButton, AtInput, AtTabBar } from "taro-ui";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { AtGrid } from "taro-ui";
|
||||
import { Swiper, SwiperItem } from "@tarojs/components";
|
||||
import { AtCard } from "taro-ui";
|
||||
import { AtImagePicker } from "taro-ui";
|
||||
import { AtButton } from "taro-ui";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Taro from "@tarojs/taro";
|
||||
import { AtAvatar } from "taro-ui";
|
||||
import { AtTag } from "taro-ui";
|
||||
import { AtList, AtListItem } from "taro-ui";
|
||||
import { AtForm, AtMessage } from "taro-ui";
|
||||
import { marked } from "marked";
|
||||
import infoText from "./info.md";
|
||||
import payText from "./pay.md";
|
||||
import { chunks as infoChunks } from "./info.md";
|
||||
import { chunks as payChunks } from "./pay.md";
|
||||
|
||||
marked.setOptions({
|
||||
breaks: true, // 是否回车换行
|
||||
gfm: true, //使用经批准的 GitHub Flavored Markdown (GFM) 规范
|
||||
// pedantic: true, //严格模式
|
||||
// highlight(code:any, lang:any) {
|
||||
// // 语法高亮
|
||||
// let val = code;
|
||||
// if (lang) {
|
||||
// val = hljs.highlight(lang, code).value;
|
||||
// } else {
|
||||
// val = hljs.highlightAuto(code).value;
|
||||
// }
|
||||
// return val;
|
||||
// },
|
||||
});
|
||||
const joinChunks = (chunks) => chunks.map(c => c.html).join("");
|
||||
|
||||
// const renderer = new marked.Renderer();
|
||||
// // 使用拓展
|
||||
// marked.use({ renderer });
|
||||
|
||||
// export default class Index extends Component {
|
||||
const Sphone = () => {
|
||||
const [markText, setmarkText] = useState();
|
||||
const [isWeChat, setisWeChat] = useState(false);
|
||||
|
||||
const [sphone, setsphone] = useState(window.localStorage.getItem("sphone"));
|
||||
|
||||
useEffect(() => {
|
||||
@@ -55,17 +24,9 @@ const Sphone = () => {
|
||||
// });
|
||||
// };
|
||||
|
||||
// 是否在微信客户端
|
||||
const isWeChatFun = () => {
|
||||
var ua = navigator.userAgent.toLowerCase();
|
||||
if (ua.indexOf("micromessenger") != -1) {
|
||||
setisWeChat(true);
|
||||
return true;
|
||||
} else {
|
||||
setisWeChat(false);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const isWeChatFun = () => {
|
||||
setisWeChat(navigator.userAgent.toLowerCase().includes("micromessenger"));
|
||||
};
|
||||
|
||||
const payh5 = (type, total_fee = 1000) => {
|
||||
// console.log("111");
|
||||
@@ -114,11 +75,11 @@ const Sphone = () => {
|
||||
return (
|
||||
<View className="Sphone">
|
||||
<View
|
||||
className="markdown-body" // 添加 GitHub Markdown 主题类
|
||||
className="markdown-body"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: marked(sphone == 1 ? payText : infoText),
|
||||
__html: joinChunks(sphone == 1 ? payChunks : infoChunks),
|
||||
}}
|
||||
></View>
|
||||
/>
|
||||
|
||||
<View className="bottomDiv"></View>
|
||||
{/* 判断有没有wxid,是否女性 */}
|
||||
|
||||
Reference in New Issue
Block a user