568 lines
22 KiB
Go
568 lines
22 KiB
Go
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()
|
||
}
|