'cbook'
This commit is contained in:
@@ -60,8 +60,8 @@ const config = {
|
|||||||
chain.module
|
chain.module
|
||||||
.rule("markdown")
|
.rule("markdown")
|
||||||
.test(/\.md$/)
|
.test(/\.md$/)
|
||||||
.use("raw-loader")
|
.use("md-chunk-loader")
|
||||||
.loader("raw-loader")
|
.loader(path.resolve(__dirname, "..", "src", "loaders", "md-chunk-loader.js"))
|
||||||
.end();
|
.end();
|
||||||
|
|
||||||
chain.module
|
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";
|
@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 {
|
.book_header_div {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -13,9 +25,10 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
height: 88px;
|
height: 88px;
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: rgba(255, 255, 255, 0.92);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(12px);
|
||||||
border-bottom: 1px solid #f0f0f0;
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
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 {
|
.Book {
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
max-width: 100%;
|
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 {
|
.markdown-body {
|
||||||
font-size: 32px !important;
|
font-size: 32px !important;
|
||||||
line-height: 1.85 !important;
|
line-height: 1.85 !important;
|
||||||
@@ -87,17 +191,27 @@
|
|||||||
font-size: 48px !important;
|
font-size: 48px !important;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: #1a1a2e;
|
color: #1a1a2e;
|
||||||
margin: 48px 0 24px;
|
margin: 56px 0 20px;
|
||||||
padding-bottom: 16px;
|
padding-bottom: 0;
|
||||||
border-bottom: 2px solid #e8e8e8;
|
border-bottom: none;
|
||||||
line-height: 1.3;
|
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 {
|
h2 {
|
||||||
font-size: 42px !important;
|
font-size: 42px !important;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #2c3e50;
|
color: #2c3e50;
|
||||||
margin: 40px 0 20px;
|
margin: 44px 0 18px;
|
||||||
padding-bottom: 12px;
|
padding-bottom: 12px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
@@ -107,14 +221,14 @@
|
|||||||
font-size: 36px !important;
|
font-size: 36px !important;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #34495e;
|
color: #34495e;
|
||||||
margin: 32px 0 16px;
|
margin: 36px 0 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h4, h5, h6 {
|
h4, h5, h6 {
|
||||||
font-size: 32px !important;
|
font-size: 32px !important;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #4a5568;
|
color: #4a5568;
|
||||||
margin: 24px 0 12px;
|
margin: 28px 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
@@ -124,13 +238,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
margin: 24px 0;
|
position: relative;
|
||||||
padding: 20px 24px;
|
margin: 28px 0;
|
||||||
border-left: 6px solid #4facfe;
|
padding: 20px 24px 20px 30px;
|
||||||
background: #f8fbff;
|
border-left: none;
|
||||||
|
background: linear-gradient(135deg, #f8fbff 0%, #f0f7ff 100%);
|
||||||
border-radius: 0 12px 12px 0;
|
border-radius: 0 12px 12px 0;
|
||||||
color: #4a5568;
|
color: #4a5568;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 6px;
|
||||||
|
background: linear-gradient(180deg, #4facfe, #00f2fe);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
&:last-child { margin-bottom: 0; }
|
&:last-child { margin-bottom: 0; }
|
||||||
@@ -138,13 +264,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
margin: 24px 0;
|
position: relative;
|
||||||
padding: 24px;
|
margin: 28px 0;
|
||||||
|
padding: 28px 24px 24px;
|
||||||
background: #1e1e2e;
|
background: #1e1e2e;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
-webkit-overflow-scrolling: touch;
|
-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 {
|
code {
|
||||||
font-family: "SF Mono", "Fira Code", Menlo, Monaco, Consolas, monospace;
|
font-family: "SF Mono", "Fira Code", Menlo, Monaco, Consolas, monospace;
|
||||||
font-size: 26px !important;
|
font-size: 26px !important;
|
||||||
@@ -192,15 +330,16 @@
|
|||||||
|
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 24px 0;
|
margin: 28px 0;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
display: block;
|
display: block;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
th {
|
th {
|
||||||
background: #f8fafc;
|
background: linear-gradient(135deg, #f8fafc, #edf2f7);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -216,20 +355,30 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
tr:nth-child(even) { background: #f8fafc; }
|
tr:nth-child(even) { background: #f8fafc; }
|
||||||
|
tr { transition: background 0.15s ease; }
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
margin: 40px 0;
|
margin: 48px 0;
|
||||||
border: none;
|
border: none;
|
||||||
border-top: 2px solid #f0f0f0;
|
height: 2px;
|
||||||
|
background: linear-gradient(90deg, transparent, #e2e8f0, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 24px auto;
|
margin: 28px auto;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
border-radius: 8px;
|
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 {
|
a {
|
||||||
@@ -237,8 +386,12 @@
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: inherit !important;
|
font-size: inherit !important;
|
||||||
border-bottom: 1px solid transparent;
|
border-bottom: 1px solid transparent;
|
||||||
transition: border-color 0.2s;
|
transition: border-color 0.2s, color 0.2s;
|
||||||
&:hover { border-bottom-color: #4facfe; }
|
|
||||||
|
&:hover {
|
||||||
|
color: #2b8de3;
|
||||||
|
border-bottom-color: #4facfe;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
strong {
|
strong {
|
||||||
@@ -258,6 +411,12 @@
|
|||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
|
transition: background 0.2s;
|
||||||
|
|
||||||
|
&[open] {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
summary {
|
summary {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 600;
|
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 {
|
.qrcode-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -332,8 +482,24 @@
|
|||||||
|
|
||||||
/* stylelint-disable */
|
/* stylelint-disable */
|
||||||
// ===== Tablet/Desktop: 768px+ =====
|
// ===== Tablet/Desktop: 768px+ =====
|
||||||
// 使用 PX (大写) 防止 Taro postcss-pxtransform 转换
|
|
||||||
@media (min-width: 768PX) {
|
@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 {
|
.book_header {
|
||||||
height: 56PX;
|
height: 56PX;
|
||||||
|
|
||||||
@@ -369,6 +535,17 @@
|
|||||||
.Book {
|
.Book {
|
||||||
padding: 30PX 60PX;
|
padding: 30PX 60PX;
|
||||||
|
|
||||||
|
.markdown-loading {
|
||||||
|
.loading-spinner {
|
||||||
|
width: 36PX;
|
||||||
|
height: 36PX;
|
||||||
|
border-width: 3PX;
|
||||||
|
}
|
||||||
|
.loading-text {
|
||||||
|
font-size: 15PX;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.book_AtDrawer .at-drawer__content {
|
.book_AtDrawer .at-drawer__content {
|
||||||
padding-top: 56PX;
|
padding-top: 56PX;
|
||||||
width: 300PX;
|
width: 300PX;
|
||||||
@@ -399,14 +576,20 @@
|
|||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 28PX !important;
|
font-size: 28PX !important;
|
||||||
margin: 32PX 0 16PX;
|
margin: 36PX 0 14PX;
|
||||||
padding-bottom: 10PX;
|
|
||||||
border-bottom-width: 2PX;
|
&::after {
|
||||||
|
width: 50PX;
|
||||||
|
height: 3PX;
|
||||||
|
margin-top: 10PX;
|
||||||
|
border-radius: 2PX;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 23PX !important;
|
font-size: 23PX !important;
|
||||||
margin: 26PX 0 12PX;
|
margin: 28PX 0 12PX;
|
||||||
|
padding-bottom: 8PX;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
@@ -428,10 +611,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
padding: 18PX;
|
padding: 22PX 18PX 18PX;
|
||||||
border-radius: 8PX;
|
border-radius: 8PX;
|
||||||
margin: 20PX 0;
|
margin: 20PX 0;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
height: 3PX;
|
||||||
|
border-radius: 8PX 8PX 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-size: 14PX !important;
|
font-size: 14PX !important;
|
||||||
}
|
}
|
||||||
@@ -449,17 +637,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
padding: 14PX 18PX;
|
padding: 14PX 18PX 14PX 22PX;
|
||||||
border-left-width: 4PX;
|
|
||||||
margin: 18PX 0;
|
margin: 18PX 0;
|
||||||
border-radius: 0 8PX 8PX 0;
|
border-radius: 0 8PX 8PX 0;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
width: 4PX;
|
||||||
|
border-radius: 2PX;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ul, ol { padding-left: 24PX; }
|
ul, ol { padding-left: 24PX; }
|
||||||
img { border-radius: 8PX; margin: 20PX auto; }
|
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; }
|
strong { font-size: inherit !important; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,13 +686,18 @@
|
|||||||
li { font-size: 17PX !important; }
|
li { font-size: 17PX !important; }
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
padding: 18PX 22PX;
|
padding: 22PX 22PX 18PX;
|
||||||
code { font-size: 14PX !important; }
|
code { font-size: 14PX !important; }
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
border-radius: 10PX;
|
border-radius: 10PX;
|
||||||
max-width: 90%;
|
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 Taro, { useRouter } from "@tarojs/taro";
|
||||||
import "./index.scss";
|
import "./index.scss";
|
||||||
import { AtButton, AtDrawer, AtIcon } from "taro-ui";
|
import { AtButton, AtDrawer, AtIcon } from "taro-ui";
|
||||||
import React, { useEffect, useState, useRef, useMemo, useCallback } from "react";
|
import React, { useEffect, useState, useRef, useCallback } from "react";
|
||||||
import { marked } from "marked";
|
import { chunks as infoChunks, headers as infoHeaders } from "./info.md";
|
||||||
import infoText from "./info.md";
|
|
||||||
import bookText from "./book.md";
|
|
||||||
import hackrobot from "../../images/hackrobot.jpg";
|
import hackrobot from "../../images/hackrobot.jpg";
|
||||||
|
|
||||||
const PB_URL = "https://pocketbase.hackrobot.cn";
|
const PB_URL = "https://pocketbase.hackrobot.cn";
|
||||||
const pb = new PocketBase(PB_URL);
|
const pb = new PocketBase(PB_URL);
|
||||||
|
|
||||||
marked.setOptions({ breaks: true, gfm: true });
|
const LazyChunk = React.memo(({ html, estimatedHeight, forceVisible }) => {
|
||||||
|
|
||||||
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 [visible, setVisible] = useState(forceVisible);
|
const [visible, setVisible] = useState(forceVisible);
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
|
|
||||||
@@ -49,28 +26,29 @@ const LazyChunk = React.memo(({ html, forceVisible }) => {
|
|||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ rootMargin: "800px 0px" }
|
{ rootMargin: "1200px 0px" }
|
||||||
);
|
);
|
||||||
observer.observe(el);
|
observer.observe(el);
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, [forceVisible, visible]);
|
}, [forceVisible, visible]);
|
||||||
|
|
||||||
if (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 Book = () => {
|
||||||
const [isWeChat, setisWeChat] = useState(false);
|
const [isWeChat, setisWeChat] = useState(false);
|
||||||
const [show, setshow] = useState(false);
|
const [show, setshow] = useState(false);
|
||||||
const [headers, setHeaders] = useState([]);
|
|
||||||
const contentRef = useRef(null);
|
const contentRef = useRef(null);
|
||||||
const [paidType, setPaidType] = useState(null);
|
const [paidType, setPaidType] = useState(null);
|
||||||
const [userId, setUserId] = useState(Taro.getStorageSync("userId") || null);
|
const [userId, setUserId] = useState(Taro.getStorageSync("userId") || null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [contentReady, setContentReady] = useState(false);
|
|
||||||
const [forceShowAll, setForceShowAll] = 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 router = useRouter();
|
||||||
const [vipbook, setVipbook] = useState(false);
|
const [vipbook, setVipbook] = useState(false);
|
||||||
|
|
||||||
@@ -126,9 +104,7 @@ const Book = () => {
|
|||||||
|
|
||||||
const isWeChatFun = () => {
|
const isWeChatFun = () => {
|
||||||
const ua = navigator.userAgent.toLowerCase();
|
const ua = navigator.userAgent.toLowerCase();
|
||||||
const result = ua.includes("micromessenger");
|
setisWeChat(ua.includes("micromessenger"));
|
||||||
setisWeChat(result);
|
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getOrCreateUserId = () => {
|
const getOrCreateUserId = () => {
|
||||||
@@ -162,18 +138,70 @@ const Book = () => {
|
|||||||
init();
|
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) => {
|
const handleJump = useCallback((text) => {
|
||||||
setForceShowAll(true);
|
setForceShowAll(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!contentRef.current) return;
|
if (!contentRef.current) return;
|
||||||
const h1Elements = contentRef.current.querySelectorAll("h1");
|
const h1s = contentRef.current.querySelectorAll("h1");
|
||||||
for (let h1 of h1Elements) {
|
for (let h1 of h1s) {
|
||||||
if (h1.textContent.trim() === text.trim()) {
|
if (h1.textContent.trim() === text.trim()) {
|
||||||
h1.scrollIntoView({ behavior: "smooth" });
|
h1.scrollIntoView({ behavior: "smooth" });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 150);
|
}, 200);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const submitPayForm = (xorpayUrl, xorpayParams) => {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<View className="reading-progress" style={{ width: `${readProgress}%` }} />
|
||||||
|
|
||||||
<View className="book_header_div" />
|
<View className="book_header_div" />
|
||||||
<View className="book_header">
|
<View className="book_header">
|
||||||
<View className="book_header_left" onClick={() => Taro.redirectTo({ url: "/" })}>
|
<View className="book_header_left" onClick={() => Taro.redirectTo({ url: "/" })}>
|
||||||
<AtIcon value="chevron-left" />
|
<AtIcon value="chevron-left" />
|
||||||
</View>
|
</View>
|
||||||
<View className="book_header_middle">数字游民</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" />
|
<AtIcon value="bullet-list" />
|
||||||
<View className="book_header_right_text pc-only">目录</View>
|
<View className="book_header_right_text pc-only">目录</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="Book">
|
<View className="Book">
|
||||||
{!contentReady ? (
|
{contentLoading ? (
|
||||||
<View className="markdown-loading">加载中...</View>
|
<View className="markdown-loading">
|
||||||
|
<View className="loading-spinner" />
|
||||||
|
<View className="loading-text">正在加载...</View>
|
||||||
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<View ref={contentRef} className="markdown-body">
|
<View ref={contentRef} className="markdown-body">
|
||||||
{chunks.map((chunk, i) => (
|
{activeChunks.map((chunk, i) => (
|
||||||
<LazyChunk
|
<LazyChunk
|
||||||
key={i}
|
key={`${isPaid}-${i}`}
|
||||||
html={chunk}
|
html={chunk.html}
|
||||||
|
estimatedHeight={chunk.estimatedHeight}
|
||||||
forceVisible={forceShowAll || i === 0}
|
forceVisible={forceShowAll || i === 0}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -260,8 +278,8 @@ const Book = () => {
|
|||||||
className="book_AtDrawer"
|
className="book_AtDrawer"
|
||||||
show={show}
|
show={show}
|
||||||
mask
|
mask
|
||||||
items={headers}
|
items={activeHeaders}
|
||||||
onItemClick={(index) => handleJump(headers[index])}
|
onItemClick={(index) => handleJump(activeHeaders[index])}
|
||||||
onClose={() => setshow(false)}
|
onClose={() => setshow(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -293,6 +311,15 @@ const Book = () => {
|
|||||||
</AtButton>
|
</AtButton>
|
||||||
)}
|
)}
|
||||||
</View>
|
</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
|
// @ts-nocheck
|
||||||
import { Component } from "react";
|
import { View } from "@tarojs/components";
|
||||||
import { View, Text, Image } from "@tarojs/components";
|
|
||||||
import "./index.scss";
|
import "./index.scss";
|
||||||
import { AtButton, AtInput, AtTabBar } from "taro-ui";
|
import { AtButton } from "taro-ui";
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { 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 Taro from "@tarojs/taro";
|
import Taro from "@tarojs/taro";
|
||||||
import { AtAvatar } from "taro-ui";
|
import { chunks as infoChunks } from "./info.md";
|
||||||
import { AtTag } from "taro-ui";
|
import { chunks as payChunks } from "./pay.md";
|
||||||
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";
|
|
||||||
|
|
||||||
marked.setOptions({
|
const joinChunks = (chunks) => chunks.map(c => c.html).join("");
|
||||||
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 renderer = new marked.Renderer();
|
|
||||||
// // 使用拓展
|
|
||||||
// marked.use({ renderer });
|
|
||||||
|
|
||||||
// export default class Index extends Component {
|
|
||||||
const Sphone = () => {
|
const Sphone = () => {
|
||||||
const [markText, setmarkText] = useState();
|
|
||||||
const [isWeChat, setisWeChat] = useState(false);
|
const [isWeChat, setisWeChat] = useState(false);
|
||||||
|
|
||||||
const [sphone, setsphone] = useState(window.localStorage.getItem("sphone"));
|
const [sphone, setsphone] = useState(window.localStorage.getItem("sphone"));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -55,17 +24,9 @@ const Sphone = () => {
|
|||||||
// });
|
// });
|
||||||
// };
|
// };
|
||||||
|
|
||||||
// 是否在微信客户端
|
const isWeChatFun = () => {
|
||||||
const isWeChatFun = () => {
|
setisWeChat(navigator.userAgent.toLowerCase().includes("micromessenger"));
|
||||||
var ua = navigator.userAgent.toLowerCase();
|
};
|
||||||
if (ua.indexOf("micromessenger") != -1) {
|
|
||||||
setisWeChat(true);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
setisWeChat(false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const payh5 = (type, total_fee = 1000) => {
|
const payh5 = (type, total_fee = 1000) => {
|
||||||
// console.log("111");
|
// console.log("111");
|
||||||
@@ -114,11 +75,11 @@ const Sphone = () => {
|
|||||||
return (
|
return (
|
||||||
<View className="Sphone">
|
<View className="Sphone">
|
||||||
<View
|
<View
|
||||||
className="markdown-body" // 添加 GitHub Markdown 主题类
|
className="markdown-body"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: marked(sphone == 1 ? payText : infoText),
|
__html: joinChunks(sphone == 1 ? payChunks : infoChunks),
|
||||||
}}
|
}}
|
||||||
></View>
|
/>
|
||||||
|
|
||||||
<View className="bottomDiv"></View>
|
<View className="bottomDiv"></View>
|
||||||
{/* 判断有没有wxid,是否女性 */}
|
{/* 判断有没有wxid,是否女性 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user