Files
Mycontent/soul-api/internal/handler/h5_read.go
2026-04-15 19:07:02 +08:00

568 lines
22 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package handler
import (
"context"
"encoding/json"
"fmt"
"html"
"net/http"
"net/url"
"regexp"
"strings"
"unicode/utf8"
"soul-api/internal/config"
"soul-api/internal/database"
"soul-api/internal/model"
"soul-api/internal/wechat"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
var (
reOgImgHTML = regexp.MustCompile(`(?is)<img[^>]+src\s*=\s*["']([^"']+)["']`)
reOgMdImg = regexp.MustCompile(`!\[[^\]]*\]\(\s*([^)]+?)\s*\)`)
)
// h5ReadShareRate 与小程序 getConfig.shareRate 同源referral_config.distributorShare
func h5ReadShareRate(db *gorm.DB) int {
rate := 90
var refRow model.SystemConfig
if err := db.Where("config_key = ?", "referral_config").First(&refRow).Error; err != nil || len(refRow.ConfigValue) == 0 {
return rate
}
var refVal map[string]interface{}
if json.Unmarshal(refRow.ConfigValue, &refVal) != nil {
return rate
}
if v, ok := refVal["distributorShare"].(float64); ok {
return int(v)
}
return rate
}
// h5EffectiveSectionUnit 与小程序章节 isFree / price 判断一致
func h5EffectiveSectionUnit(ch *model.Chapter) (isFree bool, unit float64) {
if ch.IsFree != nil && *ch.IsFree {
return true, 0
}
if ch.Price != nil {
p := *ch.Price
if p == 0 {
return true, 0
}
return false, p
}
return false, 1
}
// H5ReadPage GET /read/:id 朋友圈/外部链接落地页
// 渲染文章预览内容(按 unpaid_preview_percent 截取),底部显示「打开小程序继续阅读」按钮
// 支持 ?ref=xxx 分销参数透传
func H5ReadPage(c *gin.Context) {
sectionID := c.Param("id")
if sectionID == "" {
c.Data(http.StatusBadRequest, "text/html; charset=utf-8", []byte(h5Error("缺少文章 ID")))
return
}
ref := strings.TrimSpace(c.Query("ref"))
midQ := strings.TrimSpace(c.Query("mid"))
giftQ := strings.TrimSpace(c.Query("gift"))
reqSn := strings.TrimSpace(c.Query("requestSn"))
db := database.DB()
var ch model.Chapter
if err := db.Where("id = ?", sectionID).First(&ch).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.Data(http.StatusNotFound, "text/html; charset=utf-8", []byte(h5Error("文章不存在")))
return
}
c.Data(http.StatusInternalServerError, "text/html; charset=utf-8", []byte(h5Error("加载失败")))
return
}
percent := chapterPreviewPercent(db, &ch)
preview := previewContent(ch.Content, percent)
title := ch.SectionTitle
if title == "" {
title = ch.ChapterTitle
}
partTitle := ""
if ch.PartTitle != "" {
partTitle = ch.PartTitle
}
chapterTitle := ""
if ch.ChapterTitle != "" && ch.ChapterTitle != title {
chapterTitle = ch.ChapterTitle
}
cfg := config.Get()
appID := cfg.WechatAppID
shareRate := h5ReadShareRate(db)
isFree, priceUnit := h5EffectiveSectionUnit(&ch)
priceYuan := ""
if !isFree {
priceYuan = fmt.Sprintf("%.2f", priceUnit)
}
// 与小程序 read 页 onLoad 对齐id、ref、mid、gift、requestSn付费章带 openPay=1 便于落地后自动调起微信支付
qv := url.Values{}
qv.Set("id", sectionID)
if ref != "" {
qv.Set("ref", ref)
}
if midQ != "" {
qv.Set("mid", midQ)
}
if giftQ != "" {
qv.Set("gift", giftQ)
}
if reqSn != "" {
qv.Set("requestSn", reqSn)
}
if !isFree {
qv.Set("openPay", "1")
}
mpPath := "pages/read/read?" + qv.Encode()
publicBase := strings.TrimSuffix(strings.TrimSpace(cfg.H5ReadPublicBaseURL), "/")
if publicBase == "" {
publicBase = strings.TrimSuffix(strings.TrimSpace(cfg.BaseURL), "/")
}
if publicBase == "" {
publicBase = "https://soulapi.quwanzhi.com"
}
canonical := fmt.Sprintf("%s/read/%s", publicBase, url.PathEscape(sectionID))
if ref != "" || midQ != "" || giftQ != "" || reqSn != "" {
qc := url.Values{}
if ref != "" {
qc.Set("ref", ref)
}
if midQ != "" {
qc.Set("mid", midQ)
}
if giftQ != "" {
qc.Set("gift", giftQ)
}
if reqSn != "" {
qc.Set("requestSn", reqSn)
}
canonical += "?" + qc.Encode()
}
ogDesc := strings.TrimSpace(preview)
if ogDesc == "" {
ogDesc = title
}
if n := utf8.RuneCountInString(ogDesc); n > 120 {
ogDesc = string([]rune(ogDesc)[:120]) + "…"
}
ogImage := h5PickShareImage(ch.Content, cfg.BaseURL, cfg.H5ReadDefaultOgImage)
shareLinkJSON, _ := json.Marshal(canonical)
signURL := h5RequestSignURL(c)
jssdk := wechat.BuildJSSDKOpenLaunchPayload(context.Background(), signURL)
jssdkBytes, jerr := json.Marshal(jssdk)
if jerr != nil {
jssdkBytes = []byte(`{"ok":false}`)
}
pageHTML := h5BuildPage(h5PageData{
Title: title,
PartTitle: partTitle,
ChapterTitle: chapterTitle,
Preview: preview,
Percent: percent,
SectionID: sectionID,
Ref: ref,
AppID: appID,
MpPath: mpPath,
OgTagsHTML: h5BuildOgTags(canonical, title, ogDesc, ogImage),
ShareLinkJSON: string(shareLinkJSON),
SharePageDisplayURL: canonical,
ShareRate: shareRate,
IsFree: isFree,
PriceYuan: priceYuan,
JSSDKJSON: string(jssdkBytes),
})
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(pageHTML))
}
// h5RequestSignURL 生成与浏览器地址栏一致的 URL供 wx.config 签名;须含 path 与 query无 #
func h5RequestSignURL(c *gin.Context) string {
proto := strings.TrimSpace(c.GetHeader("X-Forwarded-Proto"))
if proto == "" {
if c.Request.TLS != nil {
proto = "https"
} else {
proto = "http"
}
} else {
proto = strings.TrimSpace(strings.Split(proto, ",")[0])
}
host := c.Request.Host
if xh := c.GetHeader("X-Forwarded-Host"); xh != "" {
host = strings.TrimSpace(strings.Split(xh, ",")[0])
}
path := c.Request.URL.Path
u := proto + "://" + host + path
if rq := c.Request.URL.RawQuery; rq != "" {
u += "?" + rq
}
if i := strings.IndexByte(u, '#'); i >= 0 {
u = u[:i]
}
return u
}
func h5AbsolutizeImage(raw, apiBase string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
}
low := strings.ToLower(raw)
if strings.HasPrefix(low, "http://") || strings.HasPrefix(low, "https://") {
return raw
}
if strings.HasPrefix(raw, "//") {
return "https:" + raw
}
ab := strings.TrimSuffix(strings.TrimSpace(apiBase), "/")
if ab == "" {
return raw
}
if strings.HasPrefix(raw, "/") {
return ab + raw
}
return ab + "/" + raw
}
func h5PickShareImage(content, apiBase, fallback string) string {
s := strings.TrimSpace(content)
if s != "" {
if m := reOgImgHTML.FindStringSubmatch(s); len(m) > 1 {
if u := h5AbsolutizeImage(m[1], apiBase); u != "" {
return u
}
}
if m := reOgMdImg.FindStringSubmatch(s); len(m) > 1 {
if u := h5AbsolutizeImage(m[1], apiBase); u != "" {
return u
}
}
}
return strings.TrimSpace(fallback)
}
func h5BuildOgTags(canonical, title, desc, ogImage string) string {
et := html.EscapeString
var b strings.Builder
b.WriteString(fmt.Sprintf(`<link rel="canonical" href="%s">`, et(canonical)))
b.WriteString(`<meta property="og:type" content="article">`)
b.WriteString(fmt.Sprintf(`<meta property="og:title" content="%s">`, et(title)))
b.WriteString(fmt.Sprintf(`<meta property="og:description" content="%s">`, et(desc)))
b.WriteString(fmt.Sprintf(`<meta property="og:url" content="%s">`, et(canonical)))
if img := strings.TrimSpace(ogImage); img != "" {
e := et(img)
b.WriteString(fmt.Sprintf(`<meta property="og:image" content="%s">`, e))
b.WriteString(fmt.Sprintf(`<meta name="twitter:image" content="%s">`, e))
}
b.WriteString(`<meta name="twitter:card" content="summary_large_image">`)
b.WriteString(fmt.Sprintf(`<meta name="twitter:title" content="%s">`, et(title)))
b.WriteString(fmt.Sprintf(`<meta name="twitter:description" content="%s">`, et(desc)))
return b.String()
}
func h5Error(msg string) string {
return fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>提示</title><style>body{font-family:-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#0f1923;color:#ccc;}</style>
</head><body><p>%s</p></body></html>`, html.EscapeString(msg))
}
type h5PageData struct {
Title, PartTitle, ChapterTitle string
Preview string
Percent int
SectionID, Ref string
AppID, MpPath string
OgTagsHTML string
ShareLinkJSON string // 已 json.Marshal 的完整分享 URL供 JS 一行赋值
SharePageDisplayURL string // 与 ShareLinkJSON 对应明文,页内展示+复制(发朋友圈用)
ShareRate int
IsFree bool
PriceYuan string // 付费时如 "1.00";免费为空
JSSDKJSON string // wx.config 用 JSON含 openTagList
}
func h5BuildPage(d h5PageData) string {
escapedTitle := html.EscapeString(d.Title)
escapedPart := html.EscapeString(d.PartTitle)
escapedChapter := html.EscapeString(d.ChapterTitle)
escapedSectionID := html.EscapeString(d.SectionID)
escapedAppID := html.EscapeString(d.AppID)
escapedMpPath := html.EscapeString(d.MpPath)
escapedSharePageURL := html.EscapeString(d.SharePageDisplayURL)
contentHTML := h5ContentToHTML(d.Preview)
subtitle := ""
if escapedPart != "" || escapedChapter != "" {
parts := []string{}
if escapedPart != "" {
parts = append(parts, escapedPart)
}
if escapedChapter != "" {
parts = append(parts, escapedChapter)
}
subtitle = fmt.Sprintf(`<p class="sub">%s</p>`, strings.Join(parts, " · "))
}
// 与 read.wxml 付费墙同一套营销句:解锁完整内容,分享得到 {rate}% 收益
marketingHTML := fmt.Sprintf(
`<div class="pw-mbox"><span class="pw-line">解锁完整内容,分享得到</span><span class="pw-pct">%d</span><span class="pw-line">%% 收益</span></div>`,
d.ShareRate,
)
// 付费:与小程序 read 单页模式一致readUi.singlePagePayButtonText + purchase-section 灰底药丸)
var primaryHTML string
if d.IsFree {
primaryHTML = `<div class="pw-primary pw-primary--full"><span class="pw-lab pw-lab--block">进入小程序阅读全文</span></div>`
} else {
btnText := html.EscapeString(fmt.Sprintf("支付 ¥%s 解锁全文", strings.TrimSpace(d.PriceYuan)))
primaryHTML = fmt.Sprintf(
`<div class="pw-primary pw-primary--sp"><span class="pw-lab pw-lab--block">%s</span></div>`,
btnText,
)
}
footerTipEsc := html.EscapeString("转发给需要的人,一起学习还能赚佣金")
lockSVG := `<div class="pw-icon-wrap" aria-hidden="true"><svg class="pw-lock" width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7 11V8a5 5 0 0 1 10 0v3" stroke="#00CED1" stroke-width="2" stroke-linecap="round" fill="none"/><rect x="5" y="11" width="14" height="11" rx="2" stroke="#00CED1" stroke-width="2" fill="none"/></svg></div>`
mpBtn := fmt.Sprintf(
`<wx-open-launch-weapp id="launch-btn" appid="%s" path="%s" style="display:block;width:100%%">
<script type="text/wxtag-template">
<style>
.pw-shell{width:100%%;box-sizing:border-box;font-family:-apple-system,BlinkMacSystemFont,"PingFang SC",sans-serif;}
.pw-card{margin:0;padding:16px 14px 18px;border-radius:16px;background:linear-gradient(135deg,#1c1c1e 0%%,#2c2c2e 100%%);border:1px solid rgba(0,206,209,0.2);}
.pw-icon-wrap{display:flex;justify-content:center;margin:0 auto 12px;}
.pw-lock{display:block;}
.pw-mbox{margin-bottom:12px;padding:14px 16px;border-radius:12px;background:#2c2c2e;border:1px solid rgba(255,255,255,0.1);text-align:center;line-height:1.55;}
.pw-line{font-size:17px;font-weight:600;color:#ffffff;display:inline;}
.pw-pct{font-size:23px;font-weight:800;color:#FFD700;display:inline;margin:0 2px;}
.pw-primary{display:flex;width:100%%;align-items:center;justify-content:center;padding:13px 14px;border-radius:999px;background:#2c2c2e;border:1px solid rgba(255,255,255,0.1);margin-bottom:0;box-sizing:border-box;}
.pw-primary--sp{/* 与 .purchase-section + .purchase-btn--compact-sp 一致 */ }
.pw-primary--full{background:linear-gradient(135deg,#00CED1 0%%,#20B2AA 100%%);border:none;box-shadow:0 4px 16px rgba(0,206,209,0.3);}
.pw-lab{font-size:15px;font-weight:600;color:#fff;}
.pw-lab--block{font-size:13px;font-weight:500;color:rgba(255,255,255,0.88);text-align:center;width:100%%;line-height:1.45;}
.pw-primary--full .pw-lab--block{font-size:15px;font-weight:600;color:#fff;}
.pw-foot{margin-top:14px;padding-top:12px;border-top:1px solid rgba(255,255,255,0.08);text-align:center;font-size:12px;color:rgba(255,255,255,0.42);line-height:1.45;}
</style>
<div class="pw-shell"><div class="pw-card">%s%s%s<p class="pw-foot">%s</p></div></div>
</script>
</wx-open-launch-weapp>`,
escapedAppID, escapedMpPath, lockSVG, marketingHTML, primaryHTML, footerTipEsc)
ctaHint := ""
if d.IsFree {
ctaHint = "微信内点下方整块区域进入小程序阅读全文"
} else {
ctaHint = fmt.Sprintf("已试读 %d%% · 点下方「支付解锁」区域进小程序,将自动调起微信支付", d.Percent)
}
var b strings.Builder
b.WriteString("<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"utf-8\">\n")
b.WriteString(`<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">` + "\n")
b.WriteString(d.OgTagsHTML)
b.WriteString("\n<title>")
b.WriteString(escapedTitle)
b.WriteString(" - 一场Soul的创业实验</title>\n<meta name=\"description\" content=\"")
b.WriteString(escapedTitle)
b.WriteString("\">\n<style>\n")
b.WriteString(`*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Helvetica Neue",sans-serif;background:#000;color:rgba(255,255,255,0.85);line-height:1.75;-webkit-font-smoothing:antialiased;padding-bottom:calc(280px + env(safe-area-inset-bottom))}
.wrap{max-width:680px;margin:0 auto;padding:20px 16px 32px}
.badge-row{margin-bottom:12px}
.chapter-badge{display:inline-block;padding:4px 10px;border-radius:8px;font-size:12px;font-weight:600;color:#00CED1;background:rgba(0,206,209,0.12);border:1px solid rgba(0,206,209,0.35)}
.hdr{padding:8px 0 20px;border-bottom:1px solid rgba(255,255,255,0.08);margin-bottom:20px}
.hdr h1{font-size:22px;color:#fff;line-height:1.45;font-weight:700}
.hdr .sub{font-size:13px;color:rgba(255,255,255,0.45);margin-top:8px}
.content{font-size:15px;color:rgba(255,255,255,0.78);line-height:1.9;word-break:break-word}
.content p{margin-bottom:12px}
.content h1,.content h2,.content h3{color:#fff;margin:20px 0 10px;font-weight:600}
.content h1{font-size:20px}
.content h2{font-size:18px}
.content h3{font-size:16px}
.content strong{color:#fff}
.content blockquote{border-left:3px solid #00CED1;padding-left:12px;margin:12px 0;color:rgba(255,255,255,0.5)}
.content code{background:#2c2c2e;padding:2px 6px;border-radius:4px;font-size:13px;color:#00CED1}
.content img{max-width:100%;border-radius:8px;margin:8px 0}
.fade{position:relative;overflow:hidden;max-height:none}
.fade::after{content:"";position:absolute;bottom:0;left:0;right:0;height:140px;background:linear-gradient(to top,rgba(0,0,0,1) 0%,transparent 100%);pointer-events:none}
.cta{position:fixed;bottom:0;left:0;right:0;background:linear-gradient(to top,rgba(0,0,0,0.98) 40%,transparent);padding:10px 16px calc(16px + env(safe-area-inset-bottom));z-index:100}
.cta-inner{max-width:680px;margin:0 auto}
.cta-hint{text-align:center;font-size:12px;color:rgba(255,255,255,0.42);margin-bottom:10px;line-height:1.45}
.share-link-bar{margin-bottom:10px;padding:10px 12px;border-radius:10px;background:rgba(255,255,255,0.05);border:1px solid rgba(0,206,209,0.25)}
.share-link-label{display:block;font-size:11px;color:rgba(255,255,255,0.5);margin-bottom:6px;text-align:center}
.share-url-text{font-size:12px;color:#00CED1;word-break:break-all;line-height:1.45;text-align:left;margin-bottom:8px}
.btn-copy-url{display:block;width:100%;padding:9px 0;text-align:center;font-size:13px;font-weight:600;color:#00CED1;background:rgba(0,206,209,0.12);border:1px solid rgba(0,206,209,0.45);border-radius:10px;cursor:pointer}
.btn-copy-url:active{opacity:0.88}
.btn-fallback{display:block;width:100%;margin-top:10px;padding:12px 0;text-align:center;background:transparent;color:rgba(255,255,255,0.88);border-radius:12px;font-size:14px;font-weight:500;border:1px solid rgba(255,255,255,0.22);cursor:pointer}
.btn-fallback:active{opacity:0.85}
.copy-toast{display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.85);color:#fff;padding:12px 24px;border-radius:8px;font-size:14px;z-index:999}
`)
b.WriteString("</style>\n</head>\n<body>\n<div class=\"wrap\">\n")
b.WriteString(`<div class="badge-row"><span class="chapter-badge">`)
b.WriteString(escapedSectionID)
b.WriteString(`</span></div>` + "\n")
b.WriteString("<div class=\"hdr\">\n<h1>")
b.WriteString(escapedTitle)
b.WriteString("</h1>\n")
b.WriteString(subtitle)
b.WriteString("\n</div>\n<div class=\"content fade\" id=\"article-content\">\n")
b.WriteString(contentHTML)
b.WriteString("\n</div></div>\n")
b.WriteString(`<div class="cta"><div class="cta-inner">` + "\n")
b.WriteString(`<p class="cta-hint">`)
b.WriteString(html.EscapeString(ctaHint))
b.WriteString(`</p>` + "\n")
b.WriteString(`<div class="share-link-bar">` + "\n")
b.WriteString(`<span class="share-link-label">发朋友圈请复制本链接(微信内打开 → 点下方「支付解锁全文」区域 → 进小程序付款)</span>` + "\n")
b.WriteString(`<p class="share-url-text" id="page-url-text">`)
b.WriteString(escapedSharePageURL)
b.WriteString(`</p>` + "\n")
b.WriteString(`<button type="button" class="btn-copy-url" onclick="copyPageUrl()">复制本页链接</button>` + "\n")
b.WriteString(`</div>` + "\n")
b.WriteString(mpBtn)
b.WriteString("\n")
b.WriteString(`<button class="btn-fallback" id="fallback-btn" style="display:none" onclick="copyAndOpen()">复制链接并打开微信</button>` + "\n")
b.WriteString(`</div></div>` + "\n")
b.WriteString(`<script type="application/json" id="wx-jssdk-data">`)
b.WriteString(d.JSSDKJSON)
b.WriteString(`</script>` + "\n")
b.WriteString(`<div class="copy-toast" id="toast">已复制,请打开微信</div>` + "\n")
b.WriteString(`<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>` + "\n<script>\n")
b.WriteString(`document.addEventListener('WeixinOpenTagsError', function (e) { console.error('[WeixinOpenTagsError]', e && e.detail); });
(function(){
try {
var el = document.getElementById('wx-jssdk-data');
if (el && el.textContent && typeof wx !== 'undefined') {
var j = JSON.parse(el.textContent);
if (j && j.ok) {
wx.config({
debug: false,
appId: j.appId,
timestamp: j.timestamp,
nonceStr: j.nonceStr,
signature: j.signature,
jsApiList: [],
openTagList: ['wx-open-launch-weapp']
});
wx.ready(function () {});
wx.error(function (err) { console.warn('[wx.config]', err); });
}
}
} catch (e) { console.warn('[wx-jssdk]', e); }
})();
`)
b.WriteString("(function(){\n")
b.WriteString(" var ua = navigator.userAgent.toLowerCase();\n")
b.WriteString(" var isWx = ua.indexOf('micromessenger') !== -1;\n")
b.WriteString(" var launchBtn = document.getElementById('launch-btn');\n")
b.WriteString(" var fallbackBtn = document.getElementById('fallback-btn');\n")
b.WriteString(" if (!isWx) {\n")
b.WriteString(" if (launchBtn) launchBtn.style.display = 'none';\n")
b.WriteString(" if (fallbackBtn) fallbackBtn.style.display = 'block';\n")
b.WriteString(" } else if (launchBtn) {\n")
b.WriteString(" launchBtn.addEventListener('error', function(e) {\n")
b.WriteString(" console.log('launch-btn error', e.detail);\n")
b.WriteString(" launchBtn.style.display = 'none';\n")
b.WriteString(" if (fallbackBtn) fallbackBtn.style.display = 'block';\n")
b.WriteString(" });\n")
b.WriteString(" }\n")
b.WriteString("})();\n")
b.WriteString("function copyPageUrl() {\n")
b.WriteString(" var link = ")
b.WriteString(d.ShareLinkJSON)
b.WriteString(";\n")
b.WriteString(` if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(link).then(function(){ showToast('链接已复制,可到朋友圈粘贴'); });
} else {
var ta = document.createElement('textarea');
ta.value = link;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
showToast('链接已复制,可到朋友圈粘贴');
}
}
`)
b.WriteString("function copyAndOpen() {\n")
b.WriteString(" var link = ")
b.WriteString(d.ShareLinkJSON)
b.WriteString(";\n")
b.WriteString(` if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(link).then(function(){ showToast('已复制,请打开微信'); });
} else {
var ta = document.createElement('textarea');
ta.value = link;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
showToast('已复制,请打开微信');
}
}
function showToast(msg) {
var t = document.getElementById('toast');
if (msg) t.textContent = msg;
t.style.display = 'block';
setTimeout(function(){ t.style.display = 'none'; t.textContent = '已复制,请打开微信'; }, 2200);
}
`)
b.WriteString("</script>\n</body>\n</html>")
return b.String()
}
func h5ContentToHTML(content string) string {
if content == "" {
return ""
}
if strings.HasPrefix(content, "<") && strings.Contains(content, "</") {
return content
}
lines := strings.Split(content, "\n")
var sb strings.Builder
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
if strings.HasPrefix(trimmed, "### ") {
sb.WriteString("<h3>")
sb.WriteString(html.EscapeString(trimmed[4:]))
sb.WriteString("</h3>")
} else if strings.HasPrefix(trimmed, "## ") {
sb.WriteString("<h2>")
sb.WriteString(html.EscapeString(trimmed[3:]))
sb.WriteString("</h2>")
} else if strings.HasPrefix(trimmed, "# ") {
sb.WriteString("<h1>")
sb.WriteString(html.EscapeString(trimmed[2:]))
sb.WriteString("</h1>")
} else if strings.HasPrefix(trimmed, "> ") {
sb.WriteString("<blockquote>")
sb.WriteString(html.EscapeString(trimmed[2:]))
sb.WriteString("</blockquote>")
} else {
sb.WriteString("<p>")
sb.WriteString(html.EscapeString(trimmed))
sb.WriteString("</p>")
}
}
return sb.String()
}