同步进程
This commit is contained in:
@@ -684,7 +684,7 @@ Page({
|
||||
goToSuperArticleDetail(e) {
|
||||
const id = e.currentTarget?.dataset?.id
|
||||
if (!id) return
|
||||
wx.navigateTo({ url: `/pages/super-article-detail/super-article-detail?id=${encodeURIComponent(String(id))}` })
|
||||
wx.navigateTo({ url: `/pages/read/read?superArticleId=${encodeURIComponent(String(id))}` })
|
||||
},
|
||||
|
||||
/** 有档案块(职业画像/擅长/故事/互助)或动态(含加载中)时展示 Tab 区 */
|
||||
|
||||
@@ -20,6 +20,7 @@ const contentParser = require('../../utils/contentParser.js')
|
||||
const { trackClick, trackViewChapter } = require('../../utils/trackClick')
|
||||
const { checkAndExecute, executeRule, getChapterReadMarketingRule } = require('../../utils/ruleEngine')
|
||||
const soulBridge = require('../../utils/soulBridge.js')
|
||||
const superArticleDetailUtil = require('../../utils/superArticleDetail.js')
|
||||
|
||||
const app = getApp()
|
||||
const mpPagePopups = require('../../utils/mpPagePopups.js')
|
||||
@@ -218,7 +219,17 @@ Page({
|
||||
// 系统信息
|
||||
statusBarHeight: 44,
|
||||
navBarHeight: 88,
|
||||
|
||||
|
||||
/** chapter:书籍章节;super_article:超级个体动态(与独立动态详情合并为本页) */
|
||||
pageMode: 'chapter',
|
||||
superArticle: null,
|
||||
superArticleId: '',
|
||||
superGalleryItems: [],
|
||||
superLoading: true,
|
||||
superAuditTip: '',
|
||||
superAuditRejectReason: '',
|
||||
superShowAuditEdit: false,
|
||||
|
||||
// 章节信息
|
||||
sectionId: '',
|
||||
section: null,
|
||||
@@ -471,6 +482,98 @@ Page({
|
||||
if (!sp) {
|
||||
this.refreshPay365MarketingFromRules()
|
||||
}
|
||||
if (this.data.pageMode === 'super_article' && this.data.superArticleId) {
|
||||
this._superArticleShowCount = (this._superArticleShowCount || 0) + 1
|
||||
if (this._superArticleShowCount > 1) {
|
||||
this.loadSuperArticleDetail(this.data.superArticleId)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 超级个体动态:与章节阅读共用本页(query superArticleId / saId)
|
||||
*/
|
||||
async _bootstrapSuperArticlePage(idStr) {
|
||||
this._superArticleShowCount = 0
|
||||
this.setData({
|
||||
pageMode: 'super_article',
|
||||
superArticleId: String(idStr),
|
||||
superLoading: true,
|
||||
loading: false,
|
||||
accessState: 'unknown',
|
||||
sectionId: '',
|
||||
sectionMid: null,
|
||||
superArticle: null,
|
||||
superGalleryItems: [],
|
||||
superAuditTip: '',
|
||||
superAuditRejectReason: '',
|
||||
superShowAuditEdit: false,
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44,
|
||||
navBarHeight: app.globalData.navBarHeight || 88,
|
||||
})
|
||||
await this.loadSuperArticleDetail(idStr)
|
||||
},
|
||||
|
||||
async loadSuperArticleDetail(id) {
|
||||
this.setData({ superLoading: true })
|
||||
const baseUrl = String((app.globalData && app.globalData.baseUrl) || '').replace(/\/$/, '')
|
||||
try {
|
||||
const viewerId = app.globalData.userInfo?.id
|
||||
const q =
|
||||
viewerId != null && String(viewerId).trim() !== ''
|
||||
? `?viewerUserId=${encodeURIComponent(String(viewerId).trim())}`
|
||||
: ''
|
||||
const res = await app.request({
|
||||
url: `/api/miniprogram/super/articles/${encodeURIComponent(String(id))}${q}`,
|
||||
silent: true,
|
||||
})
|
||||
const article = res?.data
|
||||
if (!article) {
|
||||
this.setData({ superLoading: false, superArticle: null, superGalleryItems: [] })
|
||||
return
|
||||
}
|
||||
const audit = superArticleDetailUtil.buildSuperArticleAuditMeta(article, app.globalData.userInfo?.id)
|
||||
const galleryItems = superArticleDetailUtil.buildGalleryItems(article.images, baseUrl)
|
||||
this.setData({
|
||||
superArticle: Object.assign({}, article, {
|
||||
createdAtText: superArticleDetailUtil.formatSuperArticleTime(article.createdAt),
|
||||
contentHtml: superArticleDetailUtil.renderSuperArticleHtml(article.content),
|
||||
}),
|
||||
superGalleryItems: galleryItems,
|
||||
superAuditTip: audit.auditTip,
|
||||
superAuditRejectReason: audit.auditRejectReason,
|
||||
superShowAuditEdit: audit.showAuditEdit,
|
||||
superLoading: false,
|
||||
})
|
||||
} catch (_) {
|
||||
this.setData({ superLoading: false, superArticle: null, superGalleryItems: [] })
|
||||
}
|
||||
},
|
||||
|
||||
goSuperArticleEdit() {
|
||||
const id = this.data.superArticleId
|
||||
if (!id) return
|
||||
wx.navigateTo({
|
||||
url: `/pages/super-article-editor/super-article-editor?id=${encodeURIComponent(String(id))}`,
|
||||
})
|
||||
},
|
||||
|
||||
onSuperGalleryImgError(e) {
|
||||
const idx = Number(e.currentTarget.dataset.idx)
|
||||
if (Number.isNaN(idx)) return
|
||||
this.setData({ [`superGalleryItems[${idx}].loadFailed`]: true })
|
||||
},
|
||||
|
||||
previewSuperGallery(e) {
|
||||
const current = e.currentTarget.dataset.src || ''
|
||||
const urls = (this.data.superGalleryItems || [])
|
||||
.filter((x) => x.src && !x.loadFailed)
|
||||
.map((x) => x.src)
|
||||
if (!urls.length) return
|
||||
wx.previewImage({
|
||||
urls,
|
||||
current: current || urls[0],
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -478,6 +581,7 @@ Page({
|
||||
* 修复:管理端取消超级个体后,若仍用订单兜底 VIP,需配合后端 isVipEffective;同时刷新端上 showPaywall/全文裁切。
|
||||
*/
|
||||
async _refreshAccessOnShow() {
|
||||
if (this.data.pageMode === 'super_article') return
|
||||
const id = this.data.sectionId
|
||||
if (!id || this.data.loading || this.data.readSinglePageMode) return
|
||||
const uid = app.globalData.userInfo?.id
|
||||
@@ -521,6 +625,13 @@ Page({
|
||||
async onLoad(options) {
|
||||
wx.showShareMenu({ menus: ['shareAppMessage', 'shareTimeline'] })
|
||||
|
||||
const superRaw = (options && (options.superArticleId || options.saId)) || ''
|
||||
const superArticleCandidate = String(superRaw).trim()
|
||||
if (superArticleCandidate && /^\d+$/.test(superArticleCandidate)) {
|
||||
await this._bootstrapSuperArticlePage(superArticleCandidate)
|
||||
return
|
||||
}
|
||||
|
||||
// 预加载:core+auditMode(getConfig)+ read-extras 懒加载(linkTags、linkedMiniprograms)
|
||||
Promise.all([
|
||||
app.getConfig(),
|
||||
@@ -1510,6 +1621,16 @@ Page({
|
||||
|
||||
// 分享到微信 - 自动带分享人ID
|
||||
onShareAppMessage(e) {
|
||||
if (this.data.pageMode === 'super_article') {
|
||||
const a = this.data.superArticle
|
||||
const sid = this.data.superArticleId
|
||||
const q = sid ? `superArticleId=${encodeURIComponent(String(sid))}` : ''
|
||||
return {
|
||||
title: a && a.title ? a.title : '动态',
|
||||
path: sid ? `/pages/read/read?${q}` : '/pages/read/read',
|
||||
}
|
||||
}
|
||||
|
||||
trackClick('read', 'btn_click', '分享_' + this.data.sectionId)
|
||||
const { section, sectionId, sectionMid } = this.data
|
||||
const ref = app.getMyReferralCode()
|
||||
@@ -1572,6 +1693,16 @@ Page({
|
||||
|
||||
// 分享到朋友圈:带文章标题,过长时截断
|
||||
onShareTimeline() {
|
||||
if (this.data.pageMode === 'super_article') {
|
||||
const a = this.data.superArticle
|
||||
const sid = this.data.superArticleId
|
||||
const query = sid ? `superArticleId=${encodeURIComponent(String(sid))}` : ''
|
||||
return {
|
||||
title: a && a.title ? a.title : '动态',
|
||||
query,
|
||||
}
|
||||
}
|
||||
|
||||
const { section, sectionId, sectionMid, chapterTitle, readUi } = this.data
|
||||
const ref = app.getMyReferralCode()
|
||||
const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}`
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<!--pages/read/read.wxml-->
|
||||
<!--卡若创业派对 - 阅读页-->
|
||||
<!--卡若创业派对 - 阅读页(章节 / 超级个体动态共用;动态:query superArticleId)-->
|
||||
<view class="page">
|
||||
<block wx:if="{{pageMode === 'chapter'}}">
|
||||
<!-- 阅读进度条 -->
|
||||
<view class="progress-bar-fixed" style="top: {{statusBarHeight}}px;">
|
||||
<view class="progress-fill" style="width: {{readingProgress}}%;"></view>
|
||||
@@ -512,4 +513,56 @@
|
||||
<text class="pay365-anchor-float-text">{{pay365AnchorLabel}}</text>
|
||||
<text class="pay365-anchor-float-chevron">›</text>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<block wx:elif="{{pageMode === 'super_article'}}">
|
||||
<view class="read-super-nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="read-super-nav-back" bindtap="goBack">
|
||||
<icon name="chevron-left" size="44" color="rgba(255,255,255,0.85)"></icon>
|
||||
</view>
|
||||
<text class="read-super-nav-title">文章详情</text>
|
||||
<view class="read-super-nav-placeholder"></view>
|
||||
</view>
|
||||
<view class="read-super-nav-offset" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<view class="read-super-loading" wx:if="{{superLoading}}">
|
||||
<text class="read-super-loading-text">加载中…</text>
|
||||
</view>
|
||||
|
||||
<view class="read-super-body" wx:elif="{{superArticle}}">
|
||||
<view class="read-super-audit-banner" wx:if="{{superAuditTip}}">
|
||||
<text class="read-super-audit-main">{{superAuditTip}}</text>
|
||||
<text class="read-super-audit-sub" wx:if="{{superAuditRejectReason}}">{{superAuditRejectReason}}</text>
|
||||
<view wx:if="{{superShowAuditEdit}}" class="read-super-audit-edit-btn" bindtap="goSuperArticleEdit">重新编辑并提交</view>
|
||||
</view>
|
||||
<text class="read-super-title" wx:if="{{superArticle.title}}">{{superArticle.title}}</text>
|
||||
<text class="read-super-title read-super-title-muted" wx:else>动态</text>
|
||||
<text class="read-super-meta">{{superArticle.authorNickname || '超级个体'}} · {{superArticle.createdAtText}}</text>
|
||||
|
||||
<view class="read-super-gallery" wx:if="{{superGalleryItems.length}}">
|
||||
<view class="read-super-gallery-cell" wx:for="{{superGalleryItems}}" wx:key="idx">
|
||||
<block wx:if="{{item.src && !item.loadFailed}}">
|
||||
<image
|
||||
class="read-super-gallery-img"
|
||||
mode="widthFix"
|
||||
src="{{item.src}}"
|
||||
bindtap="previewSuperGallery"
|
||||
data-src="{{item.src}}"
|
||||
binderror="onSuperGalleryImgError"
|
||||
data-idx="{{index}}"
|
||||
/>
|
||||
</block>
|
||||
<view wx:else class="read-super-gallery-ph">
|
||||
<text class="read-super-gallery-ph-text">{{item.src ? '图片加载失败' : '配图缺失或地址无效'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<rich-text class="read-super-content" nodes="{{superArticle.contentHtml}}"></rich-text>
|
||||
</view>
|
||||
|
||||
<view class="read-super-empty" wx:elif="{{!superLoading}}">
|
||||
<text>动态不存在或已下线</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
@@ -1964,3 +1964,140 @@ button.action-share-native {
|
||||
color: #38bdac;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
|
||||
/* ---------- 超级个体动态(与章节共用阅读页 pageMode=super_article) ---------- */
|
||||
.read-super-nav-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16rpx;
|
||||
background: rgba(5, 11, 20, 0.92);
|
||||
border-bottom: 1rpx solid rgba(34, 211, 238, 0.12);
|
||||
}
|
||||
.read-super-nav-back,
|
||||
.read-super-nav-placeholder {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.read-super-nav-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
.read-super-nav-offset {
|
||||
width: 100%;
|
||||
}
|
||||
.read-super-loading {
|
||||
padding-top: 120rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.read-super-loading-text {
|
||||
font-size: 26rpx;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.read-super-body {
|
||||
padding: 8rpx 24rpx 48rpx;
|
||||
}
|
||||
.read-super-audit-banner {
|
||||
margin-bottom: 20rpx;
|
||||
padding: 20rpx 22rpx;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(251, 191, 36, 0.08);
|
||||
border: 1rpx solid rgba(251, 191, 36, 0.28);
|
||||
}
|
||||
.read-super-audit-main {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #fcd34d;
|
||||
font-weight: 600;
|
||||
}
|
||||
.read-super-audit-sub {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: rgba(226, 232, 240, 0.88);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.read-super-audit-edit-btn {
|
||||
margin-top: 18rpx;
|
||||
display: inline-block;
|
||||
padding: 14rpx 28rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #0b1220;
|
||||
background: linear-gradient(135deg, #00ced1, #20b2aa);
|
||||
}
|
||||
.read-super-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
display: block;
|
||||
color: #fff;
|
||||
}
|
||||
.read-super-title-muted {
|
||||
color: #94a3b8;
|
||||
font-weight: 600;
|
||||
}
|
||||
.read-super-meta {
|
||||
margin-top: 12rpx;
|
||||
font-size: 22rpx;
|
||||
color: #94a3b8;
|
||||
display: block;
|
||||
}
|
||||
.read-super-gallery {
|
||||
margin-top: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.read-super-gallery-cell {
|
||||
width: 100%;
|
||||
}
|
||||
.read-super-gallery-img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border-radius: 12rpx;
|
||||
box-sizing: border-box;
|
||||
border: 2rpx solid rgba(34, 211, 238, 0.32);
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
}
|
||||
.read-super-gallery-ph {
|
||||
min-height: 220rpx;
|
||||
border-radius: 12rpx;
|
||||
border: 2rpx dashed rgba(148, 163, 184, 0.35);
|
||||
background: rgba(15, 23, 42, 0.55);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.read-super-gallery-ph-text {
|
||||
font-size: 24rpx;
|
||||
color: #64748b;
|
||||
text-align: center;
|
||||
}
|
||||
.read-super-content {
|
||||
margin-top: 24rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.75;
|
||||
color: #e2e8f0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.read-super-empty {
|
||||
margin-top: 200rpx;
|
||||
text-align: center;
|
||||
color: #94a3b8;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
@@ -1,196 +1,16 @@
|
||||
const app = getApp()
|
||||
|
||||
function formatTime(value) {
|
||||
if (!value) return ''
|
||||
const d = new Date(value)
|
||||
if (isNaN(d.getTime())) return ''
|
||||
const m = `${d.getMonth() + 1}`.padStart(2, '0')
|
||||
const day = `${d.getDate()}`.padStart(2, '0')
|
||||
const hh = `${d.getHours()}`.padStart(2, '0')
|
||||
const mm = `${d.getMinutes()}`.padStart(2, '0')
|
||||
return `${m}-${day} ${hh}:${mm}`
|
||||
}
|
||||
|
||||
/** 解析详情接口 images:数组或 JSON 字符串 */
|
||||
function parseImagesField(images) {
|
||||
if (images == null) return []
|
||||
if (typeof images === 'string') {
|
||||
const s = images.trim()
|
||||
if (!s) return []
|
||||
try {
|
||||
const p = JSON.parse(s)
|
||||
return Array.isArray(p) ? p : []
|
||||
} catch (_) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
return Array.isArray(images) ? images : []
|
||||
}
|
||||
|
||||
/**
|
||||
* 转成小程序 <image> 可用的地址:相对路径 /uploads/... 须拼 API 域名(与 request 同源),否则真机不显示
|
||||
* Soul创业派对 - 兼容入口:动态详情已并入阅读页 pages/read/read(superArticleId)
|
||||
* 旧链接 / 分享路径仍可能指向本页,此处统一 redirect。
|
||||
*/
|
||||
function normalizeArticleImageSrc(raw) {
|
||||
const u = String(raw || '').trim()
|
||||
if (!u || u === 'undefined' || u === 'null') return ''
|
||||
if (/^https?:\/\//i.test(u)) return u
|
||||
if (u.startsWith('//')) return `https:${u}`
|
||||
if (u.startsWith('wxfile://') || u.startsWith('cloud://')) return u
|
||||
const base = String((app.globalData && app.globalData.baseUrl) || '').replace(/\/$/, '')
|
||||
if (u.startsWith('/') && base) return `${base}${u}`
|
||||
return u
|
||||
}
|
||||
|
||||
function buildGalleryItems(imagesField) {
|
||||
const rawList = parseImagesField(imagesField)
|
||||
return rawList.map((u, idx) => {
|
||||
const src = normalizeArticleImageSrc(String(u || '').trim())
|
||||
const ok =
|
||||
!!src &&
|
||||
(/^https?:\/\//i.test(src) || src.startsWith('wxfile://') || src.startsWith('cloud://'))
|
||||
return { idx, src: ok ? src : '', loadFailed: false }
|
||||
})
|
||||
}
|
||||
|
||||
function renderArticleHtml(text) {
|
||||
const src = String(text || '')
|
||||
const isHtml = /<[a-z][\s\S]*>/i.test(src)
|
||||
|
||||
if (isHtml) {
|
||||
return src.replace(/<a\b([^>]*)>/gi, (full, attrs) => {
|
||||
if (!/href\s*=/.test(attrs)) return full
|
||||
const hasStyle = /\sstyle\s*=/.test(attrs)
|
||||
const styleText = 'color:#22d3ee;text-decoration:underline;word-break:break-all;'
|
||||
if (hasStyle) {
|
||||
return `<a${attrs.replace(/\sstyle\s*=\s*(['"])(.*?)\1/i, (_m, q, s) => ` style=${q}${s};${styleText}${q}`)}>`
|
||||
}
|
||||
return `<a${attrs} style="${styleText}">`
|
||||
})
|
||||
}
|
||||
|
||||
const escaped = src
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
const withMentions = escaped.replace(/(^|[\s])(@[\u4e00-\u9fa5A-Za-z0-9_]+)/g, '$1<span style="color:#67e8f9;">$2</span>')
|
||||
const withTags = withMentions.replace(/(^|[\s])(#[^\s#]+)/g, '$1<span style="color:#5eead4;">$2</span>')
|
||||
const withLinks = withTags.replace(
|
||||
/(https?:\/\/[^\s<]+)/g,
|
||||
'<a href="$1" style="color:#22d3ee;text-decoration:underline;word-break:break-all;">$1</a>'
|
||||
)
|
||||
return withLinks.replace(/\n/g, '<br/>')
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
article: null,
|
||||
articleId: '',
|
||||
galleryItems: [],
|
||||
loading: true,
|
||||
auditTip: '',
|
||||
auditRejectReason: '',
|
||||
showAuditEdit: false,
|
||||
},
|
||||
onLoad(options) {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
const id = options?.id
|
||||
if (!id) {
|
||||
this.setData({ loading: false })
|
||||
const id = options?.id != null ? String(options.id).trim() : ''
|
||||
if (!id || !/^\d+$/.test(id)) {
|
||||
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/index/index' }) })
|
||||
return
|
||||
}
|
||||
this.loadArticle(id)
|
||||
},
|
||||
async loadArticle(id) {
|
||||
this.setData({ loading: true })
|
||||
try {
|
||||
const viewerId = app.globalData.userInfo?.id
|
||||
const q =
|
||||
viewerId != null && String(viewerId).trim() !== ''
|
||||
? `?viewerUserId=${encodeURIComponent(String(viewerId).trim())}`
|
||||
: ''
|
||||
const res = await app.request({
|
||||
url: `/api/miniprogram/super/articles/${encodeURIComponent(String(id))}${q}`,
|
||||
silent: true,
|
||||
})
|
||||
const article = res?.data
|
||||
if (!article) {
|
||||
this.setData({ loading: false, galleryItems: [] })
|
||||
return
|
||||
}
|
||||
const uid = app.globalData.userInfo?.id
|
||||
const isOwner = !!(uid != null && String(article.userId) === String(uid))
|
||||
const st = String(article.auditStatus || 'approved').toLowerCase()
|
||||
let auditTip = ''
|
||||
let auditRejectReason = ''
|
||||
let showAuditEdit = false
|
||||
if (isOwner && st === 'pending') {
|
||||
auditTip = '审核中:通过后将在动态广场展示'
|
||||
} else if (isOwner && st === 'rejected') {
|
||||
auditTip = '未通过审核'
|
||||
auditRejectReason = String(article.rejectReason || '').trim()
|
||||
showAuditEdit = true
|
||||
}
|
||||
const galleryItems = buildGalleryItems(article.images)
|
||||
this.setData({
|
||||
article: Object.assign({}, article, {
|
||||
createdAtText: formatTime(article.createdAt),
|
||||
contentHtml: renderArticleHtml(article.content),
|
||||
}),
|
||||
galleryItems,
|
||||
articleId: String(id),
|
||||
auditTip,
|
||||
auditRejectReason,
|
||||
showAuditEdit,
|
||||
loading: false,
|
||||
})
|
||||
} catch (_) {
|
||||
this.setData({ loading: false, galleryItems: [] })
|
||||
}
|
||||
},
|
||||
goEditArticle() {
|
||||
const id = this.data.articleId
|
||||
if (!id) return
|
||||
wx.navigateTo({
|
||||
url: `/pages/super-article-editor/super-article-editor?id=${encodeURIComponent(String(id))}`,
|
||||
wx.redirectTo({
|
||||
url: `/pages/read/read?superArticleId=${encodeURIComponent(id)}`,
|
||||
})
|
||||
},
|
||||
goBack() {
|
||||
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/my/my' }) })
|
||||
},
|
||||
onGalleryImgError(e) {
|
||||
const idx = Number(e.currentTarget.dataset.idx)
|
||||
if (Number.isNaN(idx)) return
|
||||
this.setData({ [`galleryItems[${idx}].loadFailed`]: true })
|
||||
},
|
||||
previewGallery(e) {
|
||||
const current = e.currentTarget.dataset.src || ''
|
||||
const urls = (this.data.galleryItems || [])
|
||||
.filter((x) => x.src && !x.loadFailed)
|
||||
.map((x) => x.src)
|
||||
if (!urls.length) return
|
||||
wx.previewImage({
|
||||
urls,
|
||||
current: current || urls[0],
|
||||
})
|
||||
},
|
||||
onShareAppMessage() {
|
||||
const a = this.data.article
|
||||
const id = this.data.articleId
|
||||
return {
|
||||
title: a && a.title ? a.title : '动态',
|
||||
path: id
|
||||
? `/pages/super-article-detail/super-article-detail?id=${encodeURIComponent(String(id))}`
|
||||
: '/pages/super-article-detail/super-article-detail',
|
||||
}
|
||||
},
|
||||
onShareTimeline() {
|
||||
const a = this.data.article
|
||||
const id = this.data.articleId
|
||||
return {
|
||||
title: a && a.title ? a.title : '动态',
|
||||
query: id ? `id=${encodeURIComponent(String(id))}` : '',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,46 +1 @@
|
||||
<view class="page">
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-back" bindtap="goBack">
|
||||
<icon name="chevron-left" size="44" color="rgba(255,255,255,0.85)"></icon>
|
||||
</view>
|
||||
<text class="nav-title">动态详情</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<view class="article-body" wx:if="{{article}}">
|
||||
<view class="audit-banner" wx:if="{{auditTip}}">
|
||||
<text class="audit-banner-main">{{auditTip}}</text>
|
||||
<text class="audit-banner-sub" wx:if="{{auditRejectReason}}">{{auditRejectReason}}</text>
|
||||
<view wx:if="{{showAuditEdit}}" class="audit-edit-btn" bindtap="goEditArticle">重新编辑并提交</view>
|
||||
</view>
|
||||
<text class="title" wx:if="{{article.title}}">{{article.title}}</text>
|
||||
<text class="title title-muted" wx:else>动态</text>
|
||||
<text class="meta">{{article.authorNickname || '超级个体'}} · {{article.createdAtText}}</text>
|
||||
|
||||
<view class="img-gallery" wx:if="{{galleryItems.length}}">
|
||||
<view class="gallery-cell" wx:for="{{galleryItems}}" wx:key="idx">
|
||||
<block wx:if="{{item.src && !item.loadFailed}}">
|
||||
<image
|
||||
class="article-gallery-img"
|
||||
mode="widthFix"
|
||||
src="{{item.src}}"
|
||||
bindtap="previewGallery"
|
||||
data-src="{{item.src}}"
|
||||
binderror="onGalleryImgError"
|
||||
data-idx="{{index}}"
|
||||
/>
|
||||
</block>
|
||||
<view wx:else class="gallery-placeholder">
|
||||
<text class="gallery-ph-text">{{item.src ? '图片加载失败' : '配图缺失或地址无效'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<rich-text class="content" nodes="{{article.contentHtml}}"></rich-text>
|
||||
</view>
|
||||
|
||||
<view class="empty" wx:if="{{!loading && !article}}">
|
||||
<text>文章不存在或已下线</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="super-article-detail-redirect"></view>
|
||||
|
||||
@@ -1,87 +1,4 @@
|
||||
.page { min-height: 100vh; background: #0b1220; color: #fff; }
|
||||
.nav-bar {
|
||||
position: fixed; left: 0; right: 0; top: 0; z-index: 10;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
height: 44px; padding: 0 24rpx; background: rgba(5, 11, 20, 0.9);
|
||||
}
|
||||
.nav-back, .nav-placeholder { width: 64rpx; }
|
||||
.nav-title { font-size: 32rpx; font-weight: 700; }
|
||||
.article-body {
|
||||
padding: 8rpx 24rpx 48rpx;
|
||||
}
|
||||
.audit-banner {
|
||||
margin-bottom: 20rpx;
|
||||
padding: 20rpx 22rpx;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(251, 191, 36, 0.08);
|
||||
border: 1rpx solid rgba(251, 191, 36, 0.28);
|
||||
}
|
||||
.audit-banner-main {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #fcd34d;
|
||||
font-weight: 600;
|
||||
}
|
||||
.audit-banner-sub {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: rgba(226, 232, 240, 0.88);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.audit-edit-btn {
|
||||
margin-top: 18rpx;
|
||||
display: inline-block;
|
||||
padding: 14rpx 28rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #0b1220;
|
||||
background: linear-gradient(135deg, #00ced1, #20b2aa);
|
||||
}
|
||||
.title { font-size: 36rpx; font-weight: 700; line-height: 1.4; display: block; }
|
||||
.title-muted { color: #94a3b8; font-weight: 600; }
|
||||
.meta { margin-top: 12rpx; font-size: 22rpx; color: #94a3b8; display: block; }
|
||||
.img-gallery {
|
||||
margin-top: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.gallery-cell {
|
||||
width: 100%;
|
||||
}
|
||||
.article-gallery-img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border-radius: 12rpx;
|
||||
box-sizing: border-box;
|
||||
border: 2rpx solid rgba(34, 211, 238, 0.32);
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
}
|
||||
.gallery-placeholder {
|
||||
min-height: 220rpx;
|
||||
margin-top: 0;
|
||||
border-radius: 12rpx;
|
||||
border: 2rpx dashed rgba(148, 163, 184, 0.35);
|
||||
background: rgba(15, 23, 42, 0.55);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.gallery-ph-text {
|
||||
font-size: 24rpx;
|
||||
color: #64748b;
|
||||
text-align: center;
|
||||
}
|
||||
.content {
|
||||
margin-top: 24rpx; font-size: 28rpx; line-height: 1.75;
|
||||
color: #e2e8f0; white-space: pre-wrap; word-break: break-word;
|
||||
}
|
||||
.empty {
|
||||
margin-top: 200rpx; text-align: center; color: #94a3b8; font-size: 26rpx;
|
||||
.super-article-detail-redirect {
|
||||
min-height: 100vh;
|
||||
background: #0b1220;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
const app = getApp()
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
const THUMB_MAX = 3
|
||||
|
||||
function relativeTime(iso) {
|
||||
if (!iso) return ''
|
||||
@@ -27,15 +28,65 @@ function auditLabel(st) {
|
||||
return '已发布'
|
||||
}
|
||||
|
||||
function parseImagesField(images) {
|
||||
if (images == null) return []
|
||||
if (typeof images === 'string') {
|
||||
const s = images.trim()
|
||||
if (!s) return []
|
||||
try {
|
||||
const p = JSON.parse(s)
|
||||
return Array.isArray(p) ? p : []
|
||||
} catch (_) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
return Array.isArray(images) ? images : []
|
||||
}
|
||||
|
||||
function normalizeArticleImageSrc(raw) {
|
||||
const u = String(raw || '').trim()
|
||||
if (!u || u === 'undefined' || u === 'null') return ''
|
||||
if (/^https?:\/\//i.test(u)) return u
|
||||
if (u.startsWith('//')) return `https:${u}`
|
||||
if (u.startsWith('wxfile://') || u.startsWith('cloud://')) return u
|
||||
const base = String((app.globalData && app.globalData.baseUrl) || '').replace(/\/$/, '')
|
||||
if (u.startsWith('/') && base) return `${base}${u}`
|
||||
return u
|
||||
}
|
||||
|
||||
function buildThumbSlots(imagesField) {
|
||||
const rawList = parseImagesField(imagesField)
|
||||
const n = Math.min(rawList.length, THUMB_MAX)
|
||||
const thumbSlots = []
|
||||
for (let i = 0; i < n; i++) {
|
||||
const srcNorm = normalizeArticleImageSrc(String(rawList[i] || '').trim())
|
||||
const ok =
|
||||
!!srcNorm &&
|
||||
(/^https?:\/\//i.test(srcNorm) ||
|
||||
srcNorm.startsWith('wxfile://') ||
|
||||
srcNorm.startsWith('cloud://'))
|
||||
thumbSlots.push({ slotIdx: i, src: ok ? srcNorm : '', loadFailed: false })
|
||||
}
|
||||
return {
|
||||
showThumbRow: rawList.length > 0,
|
||||
thumbSlots,
|
||||
thumbMoreCount: rawList.length > THUMB_MAX ? rawList.length - THUMB_MAX : 0,
|
||||
}
|
||||
}
|
||||
|
||||
function mapRows(rows) {
|
||||
return (rows || []).map((r) => {
|
||||
const st = String(r.auditStatus || 'approved').toLowerCase()
|
||||
const thumb = buildThumbSlots(r.images)
|
||||
return Object.assign({}, r, {
|
||||
auditStatus: st === 'pending' || st === 'rejected' ? st : 'approved',
|
||||
auditLabel: auditLabel(st),
|
||||
timeText: relativeTime(r.createdAt),
|
||||
preview: String(r.preview || '').trim(),
|
||||
rejectReason: String(r.rejectReason || '').trim(),
|
||||
showThumbRow: thumb.showThumbRow,
|
||||
thumbSlots: thumb.thumbSlots,
|
||||
thumbMoreCount: thumb.thumbMoreCount,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -88,7 +139,7 @@ Page({
|
||||
openDetail(e) {
|
||||
const id = e.currentTarget?.dataset?.id
|
||||
if (!id) return
|
||||
wx.navigateTo({ url: `/pages/super-article-detail/super-article-detail?id=${encodeURIComponent(String(id))}` })
|
||||
wx.navigateTo({ url: `/pages/read/read?superArticleId=${encodeURIComponent(String(id))}` })
|
||||
},
|
||||
|
||||
openEdit(e) {
|
||||
@@ -138,6 +189,26 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
onThumbError(e) {
|
||||
const rowIdx = Number(e.currentTarget.dataset.rowidx)
|
||||
const slotIdx = Number(e.currentTarget.dataset.slotidx)
|
||||
if (Number.isNaN(rowIdx) || Number.isNaN(slotIdx)) return
|
||||
this.setData({ [`list[${rowIdx}].thumbSlots[${slotIdx}].loadFailed`]: true })
|
||||
},
|
||||
|
||||
previewRowImages(e) {
|
||||
const rowIdx = Number(e.currentTarget.dataset.rowidx)
|
||||
if (Number.isNaN(rowIdx)) return
|
||||
const row = this.data.list[rowIdx]
|
||||
if (!row) return
|
||||
const urls = parseImagesField(row.images)
|
||||
.map((u) => normalizeArticleImageSrc(String(u || '').trim()))
|
||||
.filter((u) => /^https?:\/\//i.test(u))
|
||||
if (!urls.length) return
|
||||
const current = String(e.currentTarget.dataset.current || '').trim() || urls[0]
|
||||
wx.previewImage({ urls, current })
|
||||
},
|
||||
|
||||
async reload() {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId || !app.globalData.isLoggedIn) {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<text class="empty-sub">点击上方「发动态」写一条吧</text>
|
||||
</view>
|
||||
|
||||
<block wx:for="{{list}}" wx:key="id">
|
||||
<block wx:for="{{list}}" wx:for-index="rowIdx" wx:key="id">
|
||||
<view class="feed-item" hover-class="feed-item-hover" bindtap="openDetail" data-id="{{item.id}}">
|
||||
<view class="feed-head">
|
||||
<text class="audit-tag audit-{{item.auditStatus}}">{{item.auditLabel}}</text>
|
||||
@@ -34,7 +34,31 @@
|
||||
<text class="feed-title" wx:if="{{item.title}}">{{item.title}}</text>
|
||||
<text class="feed-title feed-title-fallback" wx:elif="{{item.preview}}">{{item.preview}}</text>
|
||||
<text class="feed-title feed-title-fallback" wx:else>动态</text>
|
||||
<text class="feed-teaser-hint">正文与配图请在详情页查看</text>
|
||||
|
||||
<view class="feed-thumb-row" wx:if="{{item.showThumbRow}}" catchtap="noop">
|
||||
<view class="thumb-cell" wx:for="{{item.thumbSlots}}" wx:for-item="th" wx:key="slotIdx">
|
||||
<block wx:if="{{th.src && !th.loadFailed}}">
|
||||
<image
|
||||
class="thumb-img"
|
||||
mode="aspectFill"
|
||||
src="{{th.src}}"
|
||||
data-rowidx="{{rowIdx}}"
|
||||
data-slotidx="{{index}}"
|
||||
data-current="{{th.src}}"
|
||||
binderror="onThumbError"
|
||||
bindtap="previewRowImages"
|
||||
/>
|
||||
</block>
|
||||
<view wx:else class="thumb-ph">
|
||||
<text class="thumb-ph-text">{{th.loadFailed ? '加载失败' : '无效'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{item.thumbMoreCount > 0}}" class="thumb-more">
|
||||
<text>+{{item.thumbMoreCount}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="feed-teaser-hint">正文请在详情页查看</text>
|
||||
|
||||
<view class="feed-actions" catchtap="noop">
|
||||
<view
|
||||
|
||||
@@ -168,6 +168,55 @@
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.feed-thumb-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.thumb-cell {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
background: rgba(15, 23, 42, 0.55);
|
||||
}
|
||||
|
||||
.thumb-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
border: 2rpx solid rgba(34, 211, 238, 0.32);
|
||||
}
|
||||
|
||||
.thumb-ph {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px dashed rgba(148, 163, 184, 0.35);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.thumb-ph-text {
|
||||
font-size: 10px;
|
||||
color: #64748b;
|
||||
transform: scale(0.92);
|
||||
}
|
||||
|
||||
.thumb-more {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: rgba(34, 211, 238, 0.88);
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.feed-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
@@ -178,7 +178,7 @@ Page({
|
||||
const id = e.currentTarget.dataset.id
|
||||
if (!id) return
|
||||
wx.navigateTo({
|
||||
url: `/pages/super-article-detail/super-article-detail?id=${encodeURIComponent(String(id))}`,
|
||||
url: `/pages/read/read?superArticleId=${encodeURIComponent(String(id))}`,
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
108
miniprogram/utils/superArticleDetail.js
Normal file
108
miniprogram/utils/superArticleDetail.js
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Soul创业派对 - 超级个体动态在阅读页(文章详情模式)中的解析与渲染
|
||||
*/
|
||||
|
||||
function formatSuperArticleTime(value) {
|
||||
if (!value) return ''
|
||||
const d = new Date(value)
|
||||
if (isNaN(d.getTime())) return ''
|
||||
const m = `${d.getMonth() + 1}`.padStart(2, '0')
|
||||
const day = `${d.getDate()}`.padStart(2, '0')
|
||||
const hh = `${d.getHours()}`.padStart(2, '0')
|
||||
const mm = `${d.getMinutes()}`.padStart(2, '0')
|
||||
return `${m}-${day} ${hh}:${mm}`
|
||||
}
|
||||
|
||||
function parseImagesField(images) {
|
||||
if (images == null) return []
|
||||
if (typeof images === 'string') {
|
||||
const s = images.trim()
|
||||
if (!s) return []
|
||||
try {
|
||||
const p = JSON.parse(s)
|
||||
return Array.isArray(p) ? p : []
|
||||
} catch (_) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
return Array.isArray(images) ? images : []
|
||||
}
|
||||
|
||||
function normalizeArticleImageSrc(raw, baseUrl) {
|
||||
const u = String(raw || '').trim()
|
||||
if (!u || u === 'undefined' || u === 'null') return ''
|
||||
if (/^https?:\/\//i.test(u)) return u
|
||||
if (u.startsWith('//')) return `https:${u}`
|
||||
if (u.startsWith('wxfile://') || u.startsWith('cloud://')) return u
|
||||
const base = String(baseUrl || '').replace(/\/$/, '')
|
||||
if (u.startsWith('/') && base) return `${base}${u}`
|
||||
return u
|
||||
}
|
||||
|
||||
function buildGalleryItems(imagesField, baseUrl) {
|
||||
const rawList = parseImagesField(imagesField)
|
||||
return rawList.map((url, idx) => {
|
||||
const src = normalizeArticleImageSrc(String(url || '').trim(), baseUrl)
|
||||
const ok =
|
||||
!!src &&
|
||||
(/^https?:\/\//i.test(src) || src.startsWith('wxfile://') || src.startsWith('cloud://'))
|
||||
return { idx, src: ok ? src : '', loadFailed: false }
|
||||
})
|
||||
}
|
||||
|
||||
function renderSuperArticleHtml(text) {
|
||||
const src = String(text || '')
|
||||
const isHtml = /<[a-z][\s\S]*>/i.test(src)
|
||||
|
||||
if (isHtml) {
|
||||
return src.replace(/<a\b([^>]*)>/gi, (full, attrs) => {
|
||||
if (!/href\s*=/.test(attrs)) return full
|
||||
const hasStyle = /\sstyle\s*=/.test(attrs)
|
||||
const styleText = 'color:#22d3ee;text-decoration:underline;word-break:break-all;'
|
||||
if (hasStyle) {
|
||||
return `<a${attrs.replace(/\sstyle\s*=\s*(['"])(.*?)\1/i, (_m, q, s) => ` style=${q}${s};${styleText}${q}`)}>`
|
||||
}
|
||||
return `<a${attrs} style="${styleText}">`
|
||||
})
|
||||
}
|
||||
|
||||
const escaped = src
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
const withMentions = escaped.replace(/(^|[\s])(@[\u4e00-\u9fa5A-Za-z0-9_]+)/g, '$1<span style="color:#67e8f9;">$2</span>')
|
||||
const withTags = withMentions.replace(/(^|[\s])(#[^\s#]+)/g, '$1<span style="color:#5eead4;">$2</span>')
|
||||
const withLinks = withTags.replace(
|
||||
/(https?:\/\/[^\s<]+)/g,
|
||||
'<a href="$1" style="color:#22d3ee;text-decoration:underline;word-break:break-all;">$1</a>'
|
||||
)
|
||||
return withLinks.replace(/\n/g, '<br/>')
|
||||
}
|
||||
|
||||
/** 根据详情接口 data + 当前登录用户,生成 setData 用的文案与按钮态 */
|
||||
function buildSuperArticleAuditMeta(article, viewerUserId) {
|
||||
const uid = viewerUserId
|
||||
const isOwner = !!(uid != null && article && String(article.userId) === String(uid))
|
||||
const st = String((article && article.auditStatus) || 'approved').toLowerCase()
|
||||
let auditTip = ''
|
||||
let auditRejectReason = ''
|
||||
let showAuditEdit = false
|
||||
if (isOwner && st === 'pending') {
|
||||
auditTip = '审核中:通过后将在动态广场展示'
|
||||
} else if (isOwner && st === 'rejected') {
|
||||
auditTip = '未通过审核'
|
||||
auditRejectReason = String((article && article.rejectReason) || '').trim()
|
||||
showAuditEdit = true
|
||||
}
|
||||
return { auditTip, auditRejectReason, showAuditEdit }
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
formatSuperArticleTime,
|
||||
parseImagesField,
|
||||
normalizeArticleImageSrc,
|
||||
buildGalleryItems,
|
||||
renderSuperArticleHtml,
|
||||
buildSuperArticleAuditMeta,
|
||||
}
|
||||
@@ -116,11 +116,11 @@ func H5SuperArticlePage(c *gin.Context) {
|
||||
publicBase = "https://soulapi.quwanzhi.com"
|
||||
}
|
||||
qv := url.Values{}
|
||||
qv.Set("id", idStr)
|
||||
qv.Set("superArticleId", idStr)
|
||||
if ref != "" {
|
||||
qv.Set("ref", ref)
|
||||
}
|
||||
mpPath := "pages/super-article-detail/super-article-detail?" + qv.Encode()
|
||||
mpPath := "pages/read/read?" + qv.Encode()
|
||||
|
||||
canonical := fmt.Sprintf("%s/s/%s", publicBase, url.PathEscape(idStr))
|
||||
if ref != "" {
|
||||
|
||||
Reference in New Issue
Block a user