feat: enhance member detail page with dynamic tabs and image handling
- Introduced member business tabs for profile and dynamics, improving navigation and user experience. - Implemented image parsing and normalization functions to handle article images effectively. - Updated the UI to display article thumbnails and manage loading states, enhancing visual feedback. - Refactored data synchronization methods to ensure member business tabs reflect current content accurately. This update aims to streamline member profile interactions and improve the overall presentation of member-related content.
This commit is contained in:
@@ -17,8 +17,67 @@ const mpPagePopups = require('../../utils/mpPagePopups.js')
|
||||
/** 从「我的」登录成功后 reLaunch 回本页并自动继续链接流程 */
|
||||
const LOGIN_RESUME_MEMBER_DETAIL_KEY = 'login_resume_member_detail_id'
|
||||
|
||||
const THUMB_MAX = 3
|
||||
|
||||
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 buildArticleThumbSlots(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,
|
||||
}
|
||||
}
|
||||
|
||||
Page({
|
||||
data: { statusBarHeight: 44, navBarTotalPx: 88, member: null, loading: true, isOwnProfile: false, auditMode: false, articleList: [], articleLoading: false },
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
navBarTotalPx: 88,
|
||||
member: null,
|
||||
loading: true,
|
||||
isOwnProfile: false,
|
||||
auditMode: false,
|
||||
articleList: [],
|
||||
articleLoading: false,
|
||||
memberBizTab: 'profile',
|
||||
showMemberBizTabs: false,
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
wx.showShareMenu({ withShareTimeline: true })
|
||||
@@ -140,6 +199,7 @@ Page({
|
||||
}),
|
||||
loading: false
|
||||
})
|
||||
this._syncMemberBizTabs()
|
||||
this.loadMemberArticles(String(id))
|
||||
this._scheduleResumeLinkFlowIfNeeded()
|
||||
return
|
||||
@@ -153,6 +213,7 @@ Page({
|
||||
const d = Array.isArray(res.data) ? res.data[0] : res.data
|
||||
if (d) {
|
||||
this.setData({ member: this.enrichAndFormat(d), loading: false })
|
||||
this._syncMemberBizTabs()
|
||||
this.loadMemberArticles(String(d.id || id))
|
||||
this._scheduleResumeLinkFlowIfNeeded()
|
||||
return
|
||||
@@ -180,6 +241,7 @@ Page({
|
||||
helpNeed: u.helpNeed || u.help_need,
|
||||
ckbLeadToken: u.ckbLeadToken || u.ckb_lead_token,
|
||||
}), loading: false })
|
||||
this._syncMemberBizTabs()
|
||||
this.loadMemberArticles(String(u.id || id))
|
||||
this._scheduleResumeLinkFlowIfNeeded()
|
||||
return
|
||||
@@ -187,6 +249,7 @@ Page({
|
||||
}
|
||||
} catch (e) {}
|
||||
this.setData({ loading: false })
|
||||
this._syncMemberBizTabs()
|
||||
this._resumeLinkAfterLoad = false
|
||||
},
|
||||
|
||||
@@ -623,9 +686,69 @@ Page({
|
||||
if (!id) return
|
||||
wx.navigateTo({ url: `/pages/super-article-detail/super-article-detail?id=${encodeURIComponent(String(id))}` })
|
||||
},
|
||||
|
||||
/** 有档案块(职业画像/擅长/故事/互助)或动态(含加载中)时展示 Tab 区 */
|
||||
_syncMemberBizTabs() {
|
||||
const m = this.data.member
|
||||
if (!m) {
|
||||
this.setData({ showMemberBizTabs: false })
|
||||
return
|
||||
}
|
||||
const hasCareer = !!(m.industry || m.position || m.businessScale)
|
||||
const hasProfileExtra = !!(
|
||||
m.skills ||
|
||||
m.bestMonth ||
|
||||
m.achievement ||
|
||||
m.turningPoint ||
|
||||
m.canHelp ||
|
||||
m.needHelp
|
||||
)
|
||||
const profileOk = hasCareer || hasProfileExtra
|
||||
const hasArticles = (this.data.articleList || []).length > 0
|
||||
const loading = !!this.data.articleLoading
|
||||
const show = profileOk || hasArticles || loading
|
||||
if (!show) {
|
||||
this.setData({ showMemberBizTabs: false })
|
||||
return
|
||||
}
|
||||
let tab = this.data.memberBizTab || 'profile'
|
||||
if (tab !== 'profile' && tab !== 'dynamics') tab = 'profile'
|
||||
const dynamicsOk = hasArticles || loading
|
||||
if (tab === 'profile' && !profileOk && dynamicsOk) tab = 'dynamics'
|
||||
else if (tab === 'dynamics' && !dynamicsOk && profileOk) tab = 'profile'
|
||||
this.setData({ showMemberBizTabs: true, memberBizTab: tab })
|
||||
},
|
||||
|
||||
switchMemberBizTab(e) {
|
||||
const tab = e.currentTarget?.dataset?.tab
|
||||
if (tab !== 'dynamics' && tab !== 'profile') return
|
||||
this.setData({ memberBizTab: tab })
|
||||
},
|
||||
|
||||
onMemberArticleThumbError(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({ [`articleList[${rowIdx}].thumbSlots[${slotIdx}].loadFailed`]: true })
|
||||
},
|
||||
|
||||
previewMemberArticleImages(e) {
|
||||
const rowIdx = Number(e.currentTarget.dataset.rowidx)
|
||||
if (Number.isNaN(rowIdx)) return
|
||||
const row = this.data.articleList[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 loadMemberArticles(memberId) {
|
||||
if (!memberId) return
|
||||
this.setData({ articleLoading: true })
|
||||
this.setData({ articleLoading: true, articleList: [] })
|
||||
this._syncMemberBizTabs()
|
||||
try {
|
||||
const uid = app.globalData.userInfo?.id
|
||||
const mine = !!(this.data.isOwnProfile && uid && String(memberId) === String(uid))
|
||||
@@ -646,14 +769,20 @@ Page({
|
||||
else auditLabel = '已发布'
|
||||
}
|
||||
const norm = st === 'pending' || st === 'rejected' ? st : 'approved'
|
||||
const thumb = buildArticleThumbSlots(it.images)
|
||||
return Object.assign({}, it, {
|
||||
auditStatus: mine ? norm : '',
|
||||
auditLabel,
|
||||
showThumbRow: thumb.showThumbRow,
|
||||
thumbSlots: thumb.thumbSlots,
|
||||
thumbMoreCount: thumb.thumbMoreCount,
|
||||
})
|
||||
})
|
||||
this.setData({ articleList: list, articleLoading: false })
|
||||
this._syncMemberBizTabs()
|
||||
} catch (_) {
|
||||
this.setData({ articleLoading: false, articleList: [] })
|
||||
this._syncMemberBizTabs()
|
||||
}
|
||||
},
|
||||
goBack() { getApp().goBackOrToHome() },
|
||||
|
||||
@@ -110,112 +110,163 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 超级个体文章:仅当有公开文章时展示整块(0 篇不显示 TA文章 Tab 卡片) -->
|
||||
<view class="article-spotlight" wx:if="{{articleList.length > 0}}">
|
||||
<view class="article-spotlight-head">
|
||||
<text class="article-spotlight-title">{{isOwnProfile ? '我的文章' : 'TA文章'}}</text>
|
||||
<text class="article-spotlight-count">{{articleList.length}} 篇</text>
|
||||
</view>
|
||||
<view class="article-spotlight-list">
|
||||
<view class="article-spotlight-item" wx:for="{{articleList}}" wx:key="id" data-id="{{item.id}}" bindtap="goToSuperArticleDetail">
|
||||
<view wx:if="{{isOwnProfile && item.auditStatus}}" class="article-audit-pill article-audit-{{item.auditStatus}}">{{item.auditLabel}}</view>
|
||||
<text class="article-spotlight-item-title">{{item.title}}</text>
|
||||
<text class="article-spotlight-item-preview">{{item.preview || item.content}}</text>
|
||||
<!-- 档案 | 动态(Tab:档案含职业画像/擅长/故事/互助) -->
|
||||
<view class="article-spotlight member-biz-tab-card" wx:if="{{showMemberBizTabs}}">
|
||||
<view class="mdb-tab-bar">
|
||||
<view
|
||||
class="mdb-tab {{memberBizTab === 'profile' ? 'mdb-tab-active' : ''}}"
|
||||
data-tab="profile"
|
||||
bindtap="switchMemberBizTab"
|
||||
>
|
||||
<text>{{isOwnProfile ? '我的档案' : 'TA档案'}}</text>
|
||||
</view>
|
||||
<view
|
||||
class="mdb-tab {{memberBizTab === 'dynamics' ? 'mdb-tab-active' : ''}}"
|
||||
data-tab="dynamics"
|
||||
bindtap="switchMemberBizTab"
|
||||
>
|
||||
<text>{{isOwnProfile ? '我的动态' : 'TA动态'}}</text>
|
||||
<text class="mdb-tab-badge" wx:if="{{articleList.length}}">{{articleList.length}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 一体化信息区(单卡片内分区) -->
|
||||
<view class="mono-card mono-card-compact" wx:if="{{member.industry || member.position || member.businessScale || member.skills || member.bestMonth || member.achievement || member.turningPoint || member.canHelp || member.needHelp || member.project}}">
|
||||
<!-- 职业画像 -->
|
||||
<view class="mono-sec mono-sec-tight" wx:if="{{member.industry || member.position || member.businessScale}}">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<view class="mono-sec-title-row">
|
||||
<!-- 档案:职业画像 + 我擅长 + 个人故事 + 互助需求 -->
|
||||
<view class="mdb-panel mdb-panel-profile" wx:if="{{memberBizTab === 'profile'}}">
|
||||
<view class="mono-sec mono-sec-tight" wx:if="{{member.industry || member.position || member.businessScale}}">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">职业画像</text>
|
||||
<view class="member-article-tag" wx:if="{{articleList.length > 0}}">
|
||||
<text>TA文章</text>
|
||||
</view>
|
||||
<view class="kv-grid">
|
||||
<view class="kv-cell" wx:if="{{member.industry}}">
|
||||
<text class="kv-k">行业</text>
|
||||
<text class="kv-v">{{member.industry}}</text>
|
||||
</view>
|
||||
<view class="kv-cell" wx:if="{{member.position}}">
|
||||
<text class="kv-k">职位</text>
|
||||
<text class="kv-v">{{member.position}}</text>
|
||||
</view>
|
||||
<view class="kv-cell kv-cell-full" wx:if="{{member.businessScale}}">
|
||||
<text class="kv-k">业务体量</text>
|
||||
<text class="kv-v">{{member.businessScale}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="kv-grid">
|
||||
<view class="kv-cell" wx:if="{{member.industry}}">
|
||||
<text class="kv-k">行业</text>
|
||||
<text class="kv-v">{{member.industry}}</text>
|
||||
|
||||
<view
|
||||
class="mono-divider mdb-inner-divider"
|
||||
wx:if="{{(member.industry || member.position || member.businessScale) && (member.skills || member.bestMonth || member.achievement || member.turningPoint || member.canHelp || member.needHelp)}}"
|
||||
></view>
|
||||
|
||||
<view class="mono-sec mono-sec-tight skills-showcase" wx:if="{{member.skills}}">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">我擅长</text>
|
||||
</view>
|
||||
<view class="kv-cell" wx:if="{{member.position}}">
|
||||
<text class="kv-k">职位</text>
|
||||
<text class="kv-v">{{member.position}}</text>
|
||||
<view class="skills-quote">
|
||||
<text class="skills-quote-text">{{member.skills}}</text>
|
||||
</view>
|
||||
<view class="kv-cell kv-cell-full" wx:if="{{member.businessScale}}">
|
||||
<text class="kv-k">业务体量</text>
|
||||
<text class="kv-v">{{member.businessScale}}</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="mono-divider mdb-inner-divider"
|
||||
wx:if="{{member.skills && (member.bestMonth || member.achievement || member.turningPoint || member.canHelp || member.needHelp)}}"
|
||||
></view>
|
||||
|
||||
<view class="mono-sec mono-sec-tight" wx:if="{{member.bestMonth || member.achievement || member.turningPoint}}">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">个人故事</text>
|
||||
</view>
|
||||
<view class="story story-compact" wx:if="{{member.bestMonth}}">
|
||||
<view class="story-head"><icon name="trophy" size="24" color="#FBBF24" customClass="story-icon"></icon><text class="story-q">最赚钱的一个月</text></view>
|
||||
<text class="story-a">{{member.bestMonth}}</text>
|
||||
</view>
|
||||
<view class="story-gap story-gap-tight" wx:if="{{member.bestMonth && (member.achievement || member.turningPoint)}}"></view>
|
||||
<view class="story story-compact" wx:if="{{member.achievement}}">
|
||||
<view class="story-head"><icon name="star" size="24" color="#FBBF24" customClass="story-icon"></icon><text class="story-q">最有成就感的事</text></view>
|
||||
<text class="story-a">{{member.achievement}}</text>
|
||||
</view>
|
||||
<view class="story-gap story-gap-tight" wx:if="{{member.achievement && member.turningPoint}}"></view>
|
||||
<view class="story story-compact" wx:if="{{member.turningPoint}}">
|
||||
<view class="story-head"><icon name="refresh-cw" size="24" color="#FBBF24" customClass="story-icon"></icon><text class="story-q">人生的转折点</text></view>
|
||||
<text class="story-a">{{member.turningPoint}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mono-divider mdb-inner-divider" wx:if="{{(member.bestMonth || member.achievement || member.turningPoint) && (member.canHelp || member.needHelp)}}"></view>
|
||||
|
||||
<view class="mono-sec mono-sec-tight" wx:if="{{member.canHelp || member.needHelp}}">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">互动需求</text>
|
||||
</view>
|
||||
<view class="help-grid">
|
||||
<view class="help-tile help-give" wx:if="{{member.canHelp}}">
|
||||
<text class="help-tile-tag">我能帮你</text>
|
||||
<text class="help-tile-txt">{{member.canHelp}}</text>
|
||||
</view>
|
||||
<view class="help-tile help-need" wx:if="{{member.needHelp}}">
|
||||
<text class="help-tile-tag need">我需要</text>
|
||||
<text class="help-tile-txt">{{member.needHelp}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
wx:if="{{!(member.industry || member.position || member.businessScale) && !member.skills && !(member.bestMonth || member.achievement || member.turningPoint) && !(member.canHelp || member.needHelp)}}"
|
||||
class="article-spotlight-empty mdb-panel-hint"
|
||||
>
|
||||
<text>暂未填写档案资料</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mono-divider" wx:if="{{(member.industry || member.position || member.businessScale) && member.skills}}"></view>
|
||||
|
||||
<!-- 核心能力 -->
|
||||
<view class="mono-sec mono-sec-tight skills-showcase" wx:if="{{member.skills}}">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">我擅长</text>
|
||||
</view>
|
||||
<view class="skills-quote">
|
||||
<text class="skills-quote-text">{{member.skills}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mono-divider" wx:if="{{member.skills && (member.bestMonth || member.achievement || member.turningPoint)}}"></view>
|
||||
|
||||
<!-- 个人故事 -->
|
||||
<view class="mono-sec mono-sec-tight" wx:if="{{member.bestMonth || member.achievement || member.turningPoint}}">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">个人故事</text>
|
||||
</view>
|
||||
<view class="story story-compact" wx:if="{{member.bestMonth}}">
|
||||
<view class="story-head"><icon name="trophy" size="24" color="#FBBF24" customClass="story-icon"></icon><text class="story-q">最赚钱的一个月</text></view>
|
||||
<text class="story-a">{{member.bestMonth}}</text>
|
||||
</view>
|
||||
<view class="story-gap story-gap-tight" wx:if="{{member.bestMonth && (member.achievement || member.turningPoint)}}"></view>
|
||||
<view class="story story-compact" wx:if="{{member.achievement}}">
|
||||
<view class="story-head"><icon name="star" size="24" color="#FBBF24" customClass="story-icon"></icon><text class="story-q">最有成就感的事</text></view>
|
||||
<text class="story-a">{{member.achievement}}</text>
|
||||
</view>
|
||||
<view class="story-gap story-gap-tight" wx:if="{{member.achievement && member.turningPoint}}"></view>
|
||||
<view class="story story-compact" wx:if="{{member.turningPoint}}">
|
||||
<view class="story-head"><icon name="refresh-cw" size="24" color="#FBBF24" customClass="story-icon"></icon><text class="story-q">人生的转折点</text></view>
|
||||
<text class="story-a">{{member.turningPoint}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mono-divider" wx:if="{{(member.bestMonth || member.achievement || member.turningPoint) && (member.canHelp || member.needHelp)}}"></view>
|
||||
|
||||
<!-- 互助 -->
|
||||
<view class="mono-sec mono-sec-tight" wx:if="{{member.canHelp || member.needHelp}}">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">互助需求</text>
|
||||
</view>
|
||||
<view class="help-grid">
|
||||
<view class="help-tile help-give" wx:if="{{member.canHelp}}">
|
||||
<text class="help-tile-tag">我能帮你</text>
|
||||
<text class="help-tile-txt">{{member.canHelp}}</text>
|
||||
</view>
|
||||
<view class="help-tile help-need" wx:if="{{member.needHelp}}">
|
||||
<text class="help-tile-tag need">我需要</text>
|
||||
<text class="help-tile-txt">{{member.needHelp}}</text>
|
||||
<view class="mdb-panel" wx:if="{{memberBizTab === 'dynamics'}}">
|
||||
<view wx:if="{{articleLoading}}" class="article-spotlight-empty mdb-panel-hint"><text>加载中…</text></view>
|
||||
<view wx:elif="{{articleList.length > 0}}" class="article-spotlight-list">
|
||||
<view
|
||||
class="article-spotlight-item"
|
||||
wx:for="{{articleList}}"
|
||||
wx:for-index="rowIdx"
|
||||
wx:key="id"
|
||||
data-id="{{item.id}}"
|
||||
bindtap="goToSuperArticleDetail"
|
||||
>
|
||||
<view wx:if="{{isOwnProfile && item.auditStatus}}" class="article-audit-pill article-audit-{{item.auditStatus}}">{{item.auditLabel}}</view>
|
||||
<text class="article-spotlight-item-title">{{item.title}}</text>
|
||||
<text class="article-spotlight-item-preview">{{item.preview || item.content}}</text>
|
||||
<view class="mdb-thumb-row" wx:if="{{item.showThumbRow}}" catchtap="">
|
||||
<view class="mdb-thumb-cell" wx:for="{{item.thumbSlots}}" wx:for-item="th" wx:key="slotIdx">
|
||||
<block wx:if="{{th.src && !th.loadFailed}}">
|
||||
<image
|
||||
class="mdb-thumb-img"
|
||||
mode="aspectFill"
|
||||
src="{{th.src}}"
|
||||
data-rowidx="{{rowIdx}}"
|
||||
data-slotidx="{{index}}"
|
||||
data-current="{{th.src}}"
|
||||
binderror="onMemberArticleThumbError"
|
||||
bindtap="previewMemberArticleImages"
|
||||
/>
|
||||
</block>
|
||||
<view wx:else class="mdb-thumb-ph">
|
||||
<text class="mdb-thumb-ph-text">{{th.loadFailed ? '加载失败' : '无效'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{item.thumbMoreCount > 0}}" class="mdb-thumb-more">
|
||||
<text>+{{item.thumbMoreCount}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:else class="article-spotlight-empty mdb-panel-hint"><text>暂无动态</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mono-divider" wx:if="{{(member.canHelp || member.needHelp) && member.project}}"></view>
|
||||
|
||||
<view class="mono-sec mono-sec-tight" wx:if="{{member.project}}">
|
||||
<!-- 项目介绍(单独卡片) -->
|
||||
<view class="mono-card mono-card-compact" wx:if="{{member.project}}">
|
||||
<view class="mono-sec mono-sec-tight">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">项目介绍</text>
|
||||
</view>
|
||||
<text class="proj-body proj-body-compact">{{member.project}}</text>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
<!-- 底部:分享 + 双入口(同一视觉块) -->
|
||||
|
||||
@@ -451,6 +451,96 @@
|
||||
border: 1rpx solid rgba(103, 232, 249, 0.45);
|
||||
background: rgba(14, 116, 144, 0.35);
|
||||
}
|
||||
|
||||
/* Tab:动态 | 职业画像 */
|
||||
.member-biz-tab-card {
|
||||
padding-top: 18rpx;
|
||||
}
|
||||
.mdb-tab-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 18rpx;
|
||||
padding: 0 4rpx;
|
||||
}
|
||||
.mdb-tab {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10rpx;
|
||||
padding: 16rpx 12rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: rgba(203, 213, 225, 0.75);
|
||||
background: rgba(2, 6, 23, 0.35);
|
||||
border: 1rpx solid rgba(148, 163, 184, 0.18);
|
||||
}
|
||||
.mdb-tab-active {
|
||||
color: #67e8f9;
|
||||
border-color: rgba(103, 232, 249, 0.45);
|
||||
background: rgba(14, 116, 144, 0.28);
|
||||
}
|
||||
.mdb-tab-badge {
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
color: #cffafe;
|
||||
border: 1rpx solid rgba(103, 232, 249, 0.35);
|
||||
background: rgba(14, 116, 144, 0.35);
|
||||
}
|
||||
.mdb-panel-hint {
|
||||
padding-top: 8rpx;
|
||||
}
|
||||
.mdb-inner-divider {
|
||||
margin: 14rpx 4rpx;
|
||||
}
|
||||
.mdb-thumb-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
margin-top: 14rpx;
|
||||
}
|
||||
.mdb-thumb-cell {
|
||||
width: 144rpx;
|
||||
height: 144rpx;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
background: rgba(15, 23, 42, 0.55);
|
||||
}
|
||||
.mdb-thumb-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
border: 2rpx solid rgba(34, 211, 238, 0.32);
|
||||
}
|
||||
.mdb-thumb-ph {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2rpx dashed rgba(148, 163, 184, 0.35);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.mdb-thumb-ph-text {
|
||||
font-size: 20rpx;
|
||||
color: #64748b;
|
||||
}
|
||||
.mdb-thumb-more {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: rgba(34, 211, 238, 0.88);
|
||||
padding: 0 12rpx;
|
||||
}
|
||||
|
||||
.article-spotlight-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -93,7 +93,7 @@ Page({
|
||||
|
||||
// 超级个体:快捷入口「链接轨迹」
|
||||
showSuperCrmEntry: false,
|
||||
// 超级个体:发文章入口
|
||||
// 超级个体:发动态入口
|
||||
showSuperArticleEntry: false,
|
||||
|
||||
/** 快捷入口网格列数:与 WXML 四个 wx:if 一致,≤4 时等于入口数(三项铺满)、否则为 4(自动换行) */
|
||||
@@ -165,9 +165,9 @@ Page({
|
||||
const showSuperCrmEntry = !!this.data.showSuperCrmEntry
|
||||
const showSuperArticleEntry = !!this.data.showSuperArticleEntry
|
||||
let n = 1
|
||||
if (!auditMode) n += 2
|
||||
if (!auditMode) n += 1
|
||||
if (showSuperCrmEntry) n += 1
|
||||
if (showSuperArticleEntry && !auditMode) n += 2
|
||||
if (showSuperArticleEntry && !auditMode) n += 1
|
||||
const quickEntryGridCols = n <= 4 ? n : 4
|
||||
if (this.data.quickEntryGridCols !== quickEntryGridCols) {
|
||||
this.setData({ quickEntryGridCols })
|
||||
@@ -894,7 +894,7 @@ Page({
|
||||
return
|
||||
}
|
||||
|
||||
const auditBlockedIds = { giftPay: true, wallet: true, withdrawRecords: true, superArticle: true, superArticleMine: true }
|
||||
const auditBlockedIds = { giftPay: true, wallet: true, withdrawRecords: true, superArticleMine: true }
|
||||
if (app.globalData.auditMode && auditBlockedIds[id]) {
|
||||
wx.showToast({ title: '当前为体验版,暂无法访问', icon: 'none' })
|
||||
return
|
||||
@@ -902,12 +902,11 @@ Page({
|
||||
|
||||
const routes = {
|
||||
orders: '/pages/purchases/purchases',
|
||||
giftPay: '/pages/gift-pay/list',
|
||||
giftPay: '/pages/purchases/purchases',
|
||||
referral: '/pages/referral/referral',
|
||||
withdrawRecords: '/pages/withdraw-records/withdraw-records',
|
||||
wallet: '/pages/wallet/wallet',
|
||||
superCrm: '/pages/super-crm/super-crm',
|
||||
superArticle: '/pages/super-article-editor/super-article-editor',
|
||||
superArticleMine: '/pages/super-article-mine/super-article-mine',
|
||||
settings: '/pages/settings/settings'
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快捷入口:我的订单 + 我的代付 -->
|
||||
<!-- 快捷入口:订单与代付(合并列表) -->
|
||||
<view class="card stats-card">
|
||||
<view class="card-header">
|
||||
<image class="card-icon-img" src="/assets/icons/eye-teal.svg" mode="aspectFit"/>
|
||||
@@ -100,13 +100,7 @@
|
||||
<view class="stat-icon-wrap">
|
||||
<image class="stat-icon-img" src="/assets/icons/list-teal.svg" mode="aspectFit"/>
|
||||
</view>
|
||||
<text class="stat-label">我的订单</text>
|
||||
</view>
|
||||
<view class="stat-box" hover-class="stat-box-hover" wx:if="{{!auditMode}}" bindtap="handleMenuTap" data-id="giftPay">
|
||||
<view class="stat-icon-wrap">
|
||||
<image class="stat-icon-img" src="/assets/icons/share-teal.svg" mode="aspectFit"/>
|
||||
</view>
|
||||
<text class="stat-label">我的代付</text>
|
||||
<text class="stat-label">订单与代付</text>
|
||||
</view>
|
||||
<view class="stat-box" hover-class="stat-box-hover" wx:if="{{!auditMode}}" bindtap="handleMenuTap" data-id="wallet">
|
||||
<view class="stat-icon-wrap">
|
||||
@@ -120,12 +114,6 @@
|
||||
</view>
|
||||
<text class="stat-label">链接轨迹</text>
|
||||
</view>
|
||||
<view class="stat-box" hover-class="stat-box-hover" wx:if="{{showSuperArticleEntry && !auditMode}}" bindtap="handleMenuTap" data-id="superArticle">
|
||||
<view class="stat-icon-wrap">
|
||||
<image class="stat-icon-img" src="/assets/icons/book-arrow-teal.svg" mode="aspectFit"/>
|
||||
</view>
|
||||
<text class="stat-label">发布文章</text>
|
||||
</view>
|
||||
<view class="stat-box" hover-class="stat-box-hover" wx:if="{{showSuperArticleEntry && !auditMode}}" bindtap="handleMenuTap" data-id="superArticleMine">
|
||||
<view class="stat-icon-wrap">
|
||||
<image class="stat-icon-img" src="/assets/icons/list-teal.svg" mode="aspectFit"/>
|
||||
@@ -135,7 +123,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 已解锁/充值/代付等流水已迁至「我的订单」页 -->
|
||||
<!-- 已解锁/充值/代付等流水在「订单与代付」统一列表 -->
|
||||
|
||||
<!-- 最近阅读 -->
|
||||
<view class="card recent-card">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Soul创业实验 - 订单页(已支付消费流水:章节/VIP/余额/代付等)
|
||||
* Soul创业派对 - 订单与代付(消费流水 + 我发起的代付,按时间合并)
|
||||
*/
|
||||
const app = getApp()
|
||||
const { cleanSingleLineField } = require('../../utils/contentParser.js')
|
||||
@@ -25,6 +25,46 @@ function midForSection(sectionId, bookFlat) {
|
||||
return row?.mid ?? row?.MID ?? 0
|
||||
}
|
||||
|
||||
function giftPayStatusLabel(status) {
|
||||
const s = String(status || '').toLowerCase()
|
||||
if (s === 'pending' || s === 'pending_pay') return '待支付'
|
||||
if (s === 'paid') return '已支付'
|
||||
if (s === 'refunded') return '已退款'
|
||||
if (s === 'cancelled') return '已取消'
|
||||
if (s === 'expired') return '已过期'
|
||||
return String(status || '').trim() || '--'
|
||||
}
|
||||
|
||||
function parseIsoMs(iso) {
|
||||
const t = new Date(iso || 0).getTime()
|
||||
return Number.isFinite(t) ? t : 0
|
||||
}
|
||||
|
||||
function mapGiftPayToRow(item) {
|
||||
const requestSn = String(item.requestSn || '').trim()
|
||||
if (!requestSn) return null
|
||||
const t = parseIsoMs(item.createdAt)
|
||||
const amt = Number(item.amount)
|
||||
const amountStr = Number.isFinite(amt) ? amt.toFixed(2) : '--'
|
||||
const titleRaw = cleanSingleLineField(String(item.description || '').trim())
|
||||
const title = titleRaw || '代付'
|
||||
const st = String(item.status || '').toLowerCase()
|
||||
const stLabel = giftPayStatusLabel(item.status)
|
||||
const gpPending = st === 'pending' || st === 'pending_pay'
|
||||
const gpPaid = st === 'paid'
|
||||
return {
|
||||
rowKey: `gp_${requestSn}`,
|
||||
kind: 'giftPay',
|
||||
requestSn,
|
||||
title,
|
||||
subLine: `¥${amountStr} · ${stLabel} · ${formatShortDate(t)}`,
|
||||
actionLabel: '查看',
|
||||
gpPending,
|
||||
gpPaid,
|
||||
_sortMs: t,
|
||||
}
|
||||
}
|
||||
|
||||
function classifyNav(productType, productId, mid) {
|
||||
const pt = String(productType || '').toLowerCase()
|
||||
if (pt === 'section' && productId) {
|
||||
@@ -43,7 +83,7 @@ function classifyNav(productType, productId, mid) {
|
||||
return { kind: 'page', path: '/pages/wallet/wallet', label: '余额' }
|
||||
}
|
||||
if (pt === 'gift_pay' || pt === 'gift_pay_batch') {
|
||||
return { kind: 'page', path: '/pages/gift-pay/list', label: '代付记录' }
|
||||
return { kind: 'page', path: '/pages/purchases/purchases', label: '查看' }
|
||||
}
|
||||
if (productId && (/^\d+\.\d+/.test(productId) || productId.length > 0)) {
|
||||
return { kind: 'read', id: productId, mid: mid || 0, label: '阅读' }
|
||||
@@ -72,6 +112,7 @@ function mapApiOrderToRow(item, bookFlat) {
|
||||
|
||||
return {
|
||||
rowKey: String(item.order_sn || item.id || `o_${t}`),
|
||||
kind: 'order',
|
||||
title,
|
||||
subLine: `¥${amountStr} · ${formatShortDate(t)}`,
|
||||
actionLabel: nav.label,
|
||||
@@ -120,40 +161,55 @@ Page({
|
||||
const userId = app.globalData.userInfo?.id
|
||||
|
||||
try {
|
||||
let orderRows = []
|
||||
|
||||
if (userId) {
|
||||
const res = await app.request({
|
||||
const orderRes = await app.request({
|
||||
url: `/api/miniprogram/orders?userId=${encodeURIComponent(userId)}`,
|
||||
silent: true
|
||||
})
|
||||
if (res && res.success && Array.isArray(res.data)) {
|
||||
const rows = res.data
|
||||
.map((item) => mapApiOrderToRow(item, bookFlat))
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => b._sortMs - a._sortMs)
|
||||
.map(({ _sortMs, ...rest }) => rest)
|
||||
this.setData({ allRows: rows, loading: false })
|
||||
this.applyDisplay(false)
|
||||
return
|
||||
silent: true,
|
||||
}).catch(() => null)
|
||||
if (orderRes && orderRes.success && Array.isArray(orderRes.data)) {
|
||||
orderRows = orderRes.data.map((item) => mapApiOrderToRow(item, bookFlat)).filter(Boolean)
|
||||
}
|
||||
}
|
||||
const ids = [...(app.globalData.purchasedSections || [])].reverse()
|
||||
const rows = ids.map((id, index) => {
|
||||
const mid = midForSection(id, bookFlat)
|
||||
const row = bookFlat.find((s) => s.id === id)
|
||||
const title =
|
||||
cleanSingleLineField(
|
||||
row?.sectionTitle || row?.section_title || row?.title || row?.chapterTitle || ''
|
||||
) || `章节 ${id}`
|
||||
const t = Date.now() - index * 86400000
|
||||
return {
|
||||
rowKey: `p_${id}_${index}`,
|
||||
title,
|
||||
subLine: `已解锁 · ${formatShortDate(t)}`,
|
||||
actionLabel: '阅读',
|
||||
nav: { kind: 'read', id, mid, label: '阅读' }
|
||||
|
||||
if (!orderRows.length) {
|
||||
const ids = [...(app.globalData.purchasedSections || [])].reverse()
|
||||
orderRows = ids.map((id, index) => {
|
||||
const mid = midForSection(id, bookFlat)
|
||||
const row = bookFlat.find((s) => s.id === id)
|
||||
const title =
|
||||
cleanSingleLineField(
|
||||
row?.sectionTitle || row?.section_title || row?.title || row?.chapterTitle || ''
|
||||
) || `章节 ${id}`
|
||||
const t = Date.now() - index * 86400000
|
||||
return {
|
||||
rowKey: `p_${id}_${index}`,
|
||||
kind: 'order',
|
||||
title,
|
||||
subLine: `已解锁 · ${formatShortDate(t)}`,
|
||||
actionLabel: '阅读',
|
||||
nav: { kind: 'read', id, mid, label: '阅读' },
|
||||
_sortMs: t,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let giftRows = []
|
||||
if (userId && !app.globalData.auditMode) {
|
||||
const giftRes = await app
|
||||
.request(`/api/miniprogram/gift-pay/my-requests?userId=${encodeURIComponent(userId)}`, {
|
||||
silent: true,
|
||||
})
|
||||
.catch(() => null)
|
||||
if (giftRes && giftRes.success && Array.isArray(giftRes.list)) {
|
||||
giftRows = giftRes.list.map(mapGiftPayToRow).filter(Boolean)
|
||||
}
|
||||
})
|
||||
this.setData({ allRows: rows, loading: false })
|
||||
}
|
||||
|
||||
const merged = [...orderRows, ...giftRows].sort((a, b) => b._sortMs - a._sortMs)
|
||||
const stripped = merged.map(({ _sortMs, ...rest }) => rest)
|
||||
this.setData({ allRows: stripped, loading: false })
|
||||
this.applyDisplay(false)
|
||||
} catch (e) {
|
||||
console.error('加载订单失败:', e)
|
||||
@@ -162,10 +218,17 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
onOrderRowTap(e) {
|
||||
onUnifiedRowTap(e) {
|
||||
const index = e.currentTarget.dataset.index
|
||||
const row = (this.data.displayRows || [])[index]
|
||||
if (!row || !row.nav) return
|
||||
if (!row) return
|
||||
if (row.kind === 'giftPay' && row.requestSn) {
|
||||
wx.navigateTo({
|
||||
url: `/pages/gift-pay/redemption-detail?requestSn=${encodeURIComponent(String(row.requestSn))}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!row.nav) return
|
||||
const { nav } = row
|
||||
if (nav.kind === 'read' && nav.id) {
|
||||
const q = nav.mid ? `mid=${nav.mid}` : `id=${nav.id}`
|
||||
@@ -174,7 +237,7 @@ Page({
|
||||
}
|
||||
if (nav.kind === 'page' && nav.path) {
|
||||
if (app.globalData.auditMode) {
|
||||
const block = { '/pages/vip/vip': true, '/pages/wallet/wallet': true, '/pages/gift-pay/list': true }
|
||||
const block = { '/pages/vip/vip': true, '/pages/wallet/wallet': true }
|
||||
if (block[nav.path]) {
|
||||
wx.showToast({ title: '当前为体验版,暂无法访问', icon: 'none' })
|
||||
return
|
||||
@@ -188,6 +251,40 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
async onGiftPayCancel(e) {
|
||||
if (e && typeof e.stopPropagation === 'function') e.stopPropagation()
|
||||
const requestSn = e?.currentTarget?.dataset?.sn
|
||||
if (!requestSn) return
|
||||
const ok = await new Promise((r) => {
|
||||
wx.showModal({ title: '取消代付', content: '确定取消该代付请求?', success: (res) => r(res.confirm) })
|
||||
})
|
||||
if (!ok) return
|
||||
try {
|
||||
const res = await app.request({
|
||||
url: '/api/miniprogram/gift-pay/cancel',
|
||||
method: 'POST',
|
||||
data: { requestSn, userId: app.globalData.userInfo?.id },
|
||||
})
|
||||
if (res && res.success) {
|
||||
wx.showToast({ title: '已取消', icon: 'success' })
|
||||
this.loadOrders()
|
||||
} else {
|
||||
wx.showToast({ title: res?.error || '取消失败', icon: 'none' })
|
||||
}
|
||||
} catch (_) {
|
||||
wx.showToast({ title: '取消失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
onGiftPayShare(e) {
|
||||
if (e && typeof e.stopPropagation === 'function') e.stopPropagation()
|
||||
const requestSn = e?.currentTarget?.dataset?.sn
|
||||
if (!requestSn) return
|
||||
wx.navigateTo({
|
||||
url: `/pages/gift-pay/detail?requestSn=${encodeURIComponent(String(requestSn))}`,
|
||||
})
|
||||
},
|
||||
|
||||
goBack() {
|
||||
getApp().goBackOrToHome()
|
||||
},
|
||||
@@ -195,13 +292,13 @@ Page({
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: '卡若创业派对 - 购买记录',
|
||||
title: '卡若创业派对 - 订单与代付',
|
||||
path: ref ? `/pages/purchases/purchases?ref=${ref}` : '/pages/purchases/purchases'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return { title: '卡若创业派对 - 购买记录', query: ref ? `ref=${ref}` : '' }
|
||||
return { title: '卡若创业派对 - 订单与代付', query: ref ? `ref=${ref}` : '' }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<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.8)" customClass="back-icon"></icon></view>
|
||||
<text class="nav-title">我的订单</text>
|
||||
<text class="nav-title">订单与代付</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
@@ -19,21 +19,23 @@
|
||||
<image class="order-history-icon" src="/assets/icons/unlock-muted-teal.svg" mode="aspectFit"/>
|
||||
</view>
|
||||
<view class="oh-list">
|
||||
<view
|
||||
class="oh-row"
|
||||
wx:for="{{displayRows}}"
|
||||
wx:key="rowKey"
|
||||
bindtap="onOrderRowTap"
|
||||
data-index="{{index}}"
|
||||
>
|
||||
<view class="oh-left">
|
||||
<text class="oh-index">{{index + 1}}</text>
|
||||
<view class="oh-text-wrap">
|
||||
<text class="oh-title">{{item.title}}</text>
|
||||
<text class="oh-sub" wx:if="{{item.subLine}}">{{item.subLine}}</text>
|
||||
<view class="oh-block" wx:for="{{displayRows}}" wx:key="rowKey">
|
||||
<view class="oh-row" bindtap="onUnifiedRowTap" data-index="{{index}}">
|
||||
<view class="oh-left">
|
||||
<text class="oh-kind {{item.kind === 'giftPay' ? 'oh-kind-gift' : 'oh-kind-order'}}">{{item.kind === 'giftPay' ? '代付' : '订单'}}</text>
|
||||
<view class="oh-text-wrap">
|
||||
<text class="oh-title">{{item.title}}</text>
|
||||
<text class="oh-sub" wx:if="{{item.subLine}}">{{item.subLine}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="oh-link">{{item.actionLabel}}</text>
|
||||
</view>
|
||||
<view class="oh-gift-tools" wx:if="{{item.kind === 'giftPay' && item.gpPending}}" catchtap="">
|
||||
<text class="oh-tool oh-tool-cancel" catchtap="onGiftPayCancel" data-sn="{{item.requestSn}}">取消</text>
|
||||
</view>
|
||||
<view class="oh-gift-tools" wx:if="{{item.kind === 'giftPay' && item.gpPaid}}" catchtap="">
|
||||
<text class="oh-tool" catchtap="onGiftPayShare" data-sn="{{item.requestSn}}">分享</text>
|
||||
</view>
|
||||
<text class="oh-link">{{item.actionLabel}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
@@ -49,7 +51,7 @@
|
||||
|
||||
<view class="empty" wx:else>
|
||||
<icon name="package" size="80" color="#3a3a3c" customClass="empty-icon"></icon>
|
||||
<text class="empty-text">暂无订单</text>
|
||||
<text class="empty-text">暂无订单与代付记录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -10,15 +10,54 @@
|
||||
.order-history-card { background: #1c1c1e; border-radius: 24rpx; padding: 28rpx 24rpx 16rpx; }
|
||||
.order-history-head { padding: 0 8rpx 16rpx 8rpx; }
|
||||
.order-history-icon { width: 40rpx; height: 40rpx; opacity: 0.92; }
|
||||
.oh-list { display: flex; flex-direction: column; gap: 16rpx; }
|
||||
.oh-list { display: flex; flex-direction: column; gap: 0; }
|
||||
.oh-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.oh-block:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.oh-row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 24rpx; background: #252525; border-radius: 20rpx;
|
||||
}
|
||||
.oh-row:active { opacity: 0.92; }
|
||||
.oh-left { display: flex; align-items: center; gap: 24rpx; overflow: hidden; min-width: 0; flex: 1; }
|
||||
.oh-text-wrap { display: flex; flex-direction: column; gap: 6rpx; min-width: 0; flex: 1; }
|
||||
.oh-index { font-size: 28rpx; color: #6B7280; font-family: monospace; flex-shrink: 0; }
|
||||
.oh-left { display: flex; align-items: flex-start; gap: 16rpx; overflow: hidden; min-width: 0; flex: 1; }
|
||||
.oh-text-wrap { display: flex; flex-direction: column; gap: 6rpx; min-width: 0; flex: 1; padding-top: 4rpx; }
|
||||
.oh-kind {
|
||||
font-size: 20rpx;
|
||||
padding: 6rpx 14rpx;
|
||||
border-radius: 10rpx;
|
||||
flex-shrink: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
.oh-kind-order {
|
||||
color: rgba(0, 206, 209, 0.92);
|
||||
border: 1rpx solid rgba(0, 206, 209, 0.35);
|
||||
background: rgba(0, 206, 209, 0.08);
|
||||
}
|
||||
.oh-kind-gift {
|
||||
color: #fcd34d;
|
||||
border: 1rpx solid rgba(252, 211, 77, 0.35);
|
||||
background: rgba(252, 211, 77, 0.08);
|
||||
}
|
||||
.oh-gift-tools {
|
||||
padding: 0 8rpx 4rpx 8rpx;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 32rpx;
|
||||
}
|
||||
.oh-tool {
|
||||
font-size: 24rpx;
|
||||
color: #00CED1;
|
||||
font-weight: 500;
|
||||
}
|
||||
.oh-tool-cancel {
|
||||
color: rgba(248, 113, 113, 0.95);
|
||||
}
|
||||
.oh-title { font-size: 28rpx; color: #E5E7EB; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.oh-sub { font-size: 22rpx; color: rgba(255,255,255,0.4); }
|
||||
.oh-link { font-size: 24rpx; color: #00CED1; font-weight: 500; flex-shrink: 0; margin-left: 16rpx; }
|
||||
|
||||
@@ -179,7 +179,7 @@ Page({
|
||||
const a = this.data.article
|
||||
const id = this.data.articleId
|
||||
return {
|
||||
title: a && a.title ? a.title : '文章',
|
||||
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',
|
||||
@@ -189,7 +189,7 @@ Page({
|
||||
const a = this.data.article
|
||||
const id = this.data.articleId
|
||||
return {
|
||||
title: a && a.title ? a.title : '文章',
|
||||
title: a && a.title ? a.title : '动态',
|
||||
query: id ? `id=${encodeURIComponent(String(id))}` : '',
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<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>
|
||||
<text class="nav-title">动态详情</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
@@ -54,6 +54,8 @@
|
||||
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 {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Soul创业派对 - 超级个体发文章(AI 生成 + 手动编辑发布)
|
||||
* Soul创业派对 - 超级个体发动态(AI 辅助 + 朋友圈式编辑)
|
||||
*/
|
||||
const app = getApp()
|
||||
const { uploadByApi } = require('../../utils/miniprogramUpload')
|
||||
@@ -76,7 +76,7 @@ Page({
|
||||
silent: true,
|
||||
})
|
||||
if (!(res?.success && res.data?.isSuperIndividual === true)) {
|
||||
wx.showToast({ title: '仅超级个体可发文章', icon: 'none' })
|
||||
wx.showToast({ title: '仅超级个体可发动态', icon: 'none' })
|
||||
setTimeout(() => wx.navigateBack(), 400)
|
||||
return
|
||||
}
|
||||
@@ -111,7 +111,7 @@ Page({
|
||||
const bodyImages = imgs.map((url) => ({ id: newAttachId(), url, uploading: false }))
|
||||
this.setData({
|
||||
editArticleId: String(id),
|
||||
title: String(article.title || '').trim(),
|
||||
title: '',
|
||||
content: String(article.content || ''),
|
||||
bodyImages,
|
||||
})
|
||||
@@ -129,10 +129,6 @@ Page({
|
||||
this.setData({ aiPrompt: e.detail.value || '' })
|
||||
},
|
||||
|
||||
onTitleInput(e) {
|
||||
this.setData({ title: e.detail.value || '' })
|
||||
},
|
||||
|
||||
onContentInput(e) {
|
||||
this.setData({ content: e.detail.value || '' })
|
||||
},
|
||||
@@ -470,9 +466,12 @@ Page({
|
||||
if (!res?.success) throw new Error(res?.error || '生成失败')
|
||||
const title = String(res.data?.title || '').trim()
|
||||
const content = String(res.data?.content || '').trim()
|
||||
if (!title || !content) throw new Error('生成结果不完整')
|
||||
this.setData({ title, content })
|
||||
wx.showToast({ title: '已填入标题和正文', icon: 'success' })
|
||||
let merged = content
|
||||
if (title && merged) merged = `${title}\n\n${merged}`
|
||||
else if (title) merged = title
|
||||
if (!merged.trim()) throw new Error('生成结果不完整')
|
||||
this.setData({ title: '', content: merged.trim() })
|
||||
wx.showToast({ title: '已填入正文', icon: 'success' })
|
||||
} catch (e) {
|
||||
wx.showToast({
|
||||
title: (e && e.message) || '生成失败',
|
||||
@@ -488,12 +487,11 @@ Page({
|
||||
if (this.data.auditMode || app.globalData.auditMode) return
|
||||
if (this.data.saving) return
|
||||
const userId = app.globalData.userInfo?.id
|
||||
const title = String(this.data.title || '').trim()
|
||||
const content = String(this.data.content || '').trim()
|
||||
if (!title || !content) {
|
||||
wx.showToast({ title: '请填写标题和正文', icon: 'none' })
|
||||
if (!userId) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const content = String(this.data.content || '').trim()
|
||||
if (this.data.bodyImages.some((x) => x.uploading)) {
|
||||
wx.showToast({ title: '请等待配图上传完成', icon: 'none' })
|
||||
return
|
||||
@@ -501,6 +499,10 @@ Page({
|
||||
const images = this.data.bodyImages
|
||||
.map((x) => String(x.url || '').trim())
|
||||
.filter(Boolean)
|
||||
if (!content && images.length === 0) {
|
||||
wx.showToast({ title: '请填写文字或添加配图', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const editId = String(this.data.editArticleId || '').trim()
|
||||
this.setData({ saving: true })
|
||||
wx.showLoading({ title: editId ? '提交中…' : '发布中…', mask: true })
|
||||
@@ -510,13 +512,13 @@ Page({
|
||||
res = await app.request({
|
||||
url: `/api/miniprogram/super/articles/${encodeURIComponent(editId)}`,
|
||||
method: 'PUT',
|
||||
data: { userId, title, content, images },
|
||||
data: { userId, title: '', content, images },
|
||||
})
|
||||
} else {
|
||||
res = await app.request({
|
||||
url: '/api/miniprogram/super/articles',
|
||||
method: 'POST',
|
||||
data: { userId, title, content, images },
|
||||
data: { userId, title: '', content, images },
|
||||
})
|
||||
}
|
||||
if (!res?.success) throw new Error(res?.error || '发布失败')
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
<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">{{editArticleId ? '编辑动态' : '发文章'}}</text>
|
||||
<text class="nav-title">{{editArticleId ? '编辑动态' : '发动态'}}</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<!-- AI:参考图片 + 提示词(开关:showAiWriteSection) -->
|
||||
<view wx:if="{{showAiWriteSection}}" class="card">
|
||||
<text class="section-title">AI 写文章</text>
|
||||
<text class="section-title">AI 帮你写</text>
|
||||
<text class="label">参考图片(可多选)</text>
|
||||
<view class="file-actions">
|
||||
<view class="add-file-btn" bindtap="chooseReferenceImages">+ 添加图片</view>
|
||||
@@ -29,43 +29,49 @@
|
||||
|
||||
<text class="label">提示词</text>
|
||||
<view class="textarea-wrap">
|
||||
<textarea class="textarea prompt-textarea" maxlength="2000" placeholder="描述主题、受众、语气、必须提到的要点…(可与文件二选一或组合)" value="{{aiPrompt}}" bindinput="onAiPromptInput" disabled="{{auditMode}}"></textarea>
|
||||
<textarea class="textarea prompt-textarea" maxlength="2000" placeholder="描述主题、语气、要点…(可与参考图组合)" value="{{aiPrompt}}" bindinput="onAiPromptInput" disabled="{{auditMode}}"></textarea>
|
||||
</view>
|
||||
|
||||
<view class="generate-btn {{generating ? 'generate-btn-loading' : ''}}" bindtap="generateArticle">{{generating ? '生成中…' : '生成文章'}}</view>
|
||||
<view class="generate-btn {{generating ? 'generate-btn-loading' : ''}}" bindtap="generateArticle">{{generating ? '生成中…' : '生成'}}</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<text class="section-title">编辑与发布</text>
|
||||
<text class="label">标题</text>
|
||||
<view class="input-wrap">
|
||||
<input class="input" maxlength="40" placeholder="写个标题(最多40字)" value="{{title}}" bindinput="onTitleInput" disabled="{{auditMode}}" />
|
||||
</view>
|
||||
|
||||
<text class="label">配图(可选,最多 9 张)</text>
|
||||
<view class="file-actions">
|
||||
<view class="add-file-btn" bindtap="chooseArticleImages">+ 添加配图</view>
|
||||
</view>
|
||||
<view class="body-img-grid" wx:if="{{bodyImages.length}}">
|
||||
<view wx:for="{{bodyImages}}" wx:key="id" class="body-img-cell">
|
||||
<image wx:if="{{item.url}}" class="body-img-thumb" mode="aspectFill" src="{{item.url}}" />
|
||||
<view wx:if="{{item.uploading}}" class="body-img-mask">上传中…</view>
|
||||
<text class="body-img-remove" data-id="{{item.id}}" bindtap="removeArticleImage">×</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="hint">配图会展示在正文上方(与书籍正文分离存储);单张建议不超 30MB。</text>
|
||||
|
||||
<text class="label">正文</text>
|
||||
<!-- 朋友圈式:先文字,再九宫格配图(+ 在最后) -->
|
||||
<view class="card moment-card">
|
||||
<view class="toolbar">
|
||||
<view class="tool-btn" bindtap="insertMentionSelf">@自己</view>
|
||||
<view class="tool-btn" bindtap="insertHashLink">#超链接</view>
|
||||
<view class="tool-btn" bindtap="insertHashLink">#链接</view>
|
||||
</view>
|
||||
<view class="textarea-wrap">
|
||||
<textarea class="textarea" maxlength="5000" placeholder="输入正文内容…" value="{{content}}" bindinput="onContentInput" disabled="{{auditMode}}"></textarea>
|
||||
<view class="textarea-wrap moment-textarea-wrap">
|
||||
<textarea
|
||||
class="textarea moment-textarea"
|
||||
maxlength="5000"
|
||||
placeholder="这一刻的想法…"
|
||||
value="{{content}}"
|
||||
bindinput="onContentInput"
|
||||
disabled="{{auditMode}}"
|
||||
auto-height
|
||||
show-confirm-bar="{{false}}"
|
||||
></textarea>
|
||||
</view>
|
||||
<text class="hint">提示:可直接输入 @昵称 或 #链接(https://xxx)</text>
|
||||
|
||||
<view class="submit-btn {{saving ? 'submit-btn-loading' : ''}}" bindtap="submitArticle">{{saving ? (editArticleId ? '提交中…' : '发布中…') : (editArticleId ? '提交审核' : '发布文章')}}</view>
|
||||
<view class="moment-photo-grid">
|
||||
<view wx:for="{{bodyImages}}" wx:key="id" class="moment-photo-cell">
|
||||
<image wx:if="{{item.url}}" class="moment-photo-img" mode="aspectFill" src="{{item.url}}" />
|
||||
<view wx:if="{{item.uploading}}" class="moment-photo-mask">上传中…</view>
|
||||
<text class="moment-photo-remove" data-id="{{item.id}}" catchtap="removeArticleImage">×</text>
|
||||
</view>
|
||||
<view wx:if="{{bodyImages.length < 9}}" class="moment-photo-cell moment-photo-add" bindtap="chooseArticleImages">
|
||||
<text class="moment-photo-add-plus">+</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="hint moment-hint">配图 {{bodyImages.length}}/9 · 展示在文字下方 · 单张建议不超 30MB · 可直接输入 @昵称 或 #链接(https://xxx)</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部发布(避让微信胶囊) -->
|
||||
<view wx:if="{{!auditMode}}" class="bottom-publish-bar">
|
||||
<view class="bottom-publish-btn {{saving ? 'bottom-publish-btn-disabled' : ''}}" bindtap="submitArticle">
|
||||
<text>{{saving ? (editArticleId ? '提交中…' : '发布中…') : (editArticleId ? '提交审核' : '发布')}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{auditMode}}" class="audit-mode-mask" catchtouchmove="preventMove">
|
||||
|
||||
@@ -1,11 +1,46 @@
|
||||
.page { min-height: 100vh; background: #0b1220; color: #fff; }
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #0b1220;
|
||||
color: #fff;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: calc(168rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.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; }
|
||||
.nav-back { width: 64rpx; flex-shrink: 0; display: flex; align-items: center; }
|
||||
.nav-placeholder { width: 64rpx; flex-shrink: 0; }
|
||||
.nav-title { flex: 1; text-align: center; font-size: 32rpx; font-weight: 700; }
|
||||
|
||||
.bottom-publish-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 50;
|
||||
padding: 20rpx 28rpx;
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
background: rgba(11, 18, 32, 0.96);
|
||||
border-top: 1rpx solid rgba(94, 234, 212, 0.12);
|
||||
}
|
||||
.bottom-publish-btn {
|
||||
height: 92rpx;
|
||||
border-radius: 18rpx;
|
||||
background: linear-gradient(135deg, #22d3ee, #14b8a6);
|
||||
color: #032b35;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.bottom-publish-btn-disabled {
|
||||
opacity: 0.65;
|
||||
pointer-events: none;
|
||||
}
|
||||
.card {
|
||||
margin: 24rpx; padding: 28rpx; border-radius: 24rpx;
|
||||
background: rgba(15, 23, 42, 0.86); border: 1rpx solid rgba(148, 163, 184, 0.2);
|
||||
@@ -102,13 +137,95 @@
|
||||
background: transparent;
|
||||
}
|
||||
.hint { display: block; margin-top: 10rpx; color: #94a3b8; font-size: 22rpx; }
|
||||
.submit-btn {
|
||||
margin-top: 24rpx; height: 88rpx; border-radius: 16rpx;
|
||||
background: linear-gradient(135deg, #22d3ee, #14b8a6);
|
||||
color: #032b35; font-size: 30rpx; font-weight: 700;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
|
||||
/* 发动态 · 配图宫格(参考朋友圈) */
|
||||
.moment-card .toolbar {
|
||||
margin-top: 0;
|
||||
}
|
||||
.moment-textarea-wrap {
|
||||
margin-top: 8rpx;
|
||||
padding: 20rpx 22rpx;
|
||||
}
|
||||
.moment-textarea {
|
||||
min-height: 240rpx;
|
||||
width: 100%;
|
||||
font-size: 30rpx;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.moment-photo-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
.moment-photo-cell {
|
||||
position: relative;
|
||||
width: calc((100% - 32rpx) / 3);
|
||||
padding-bottom: calc((100% - 32rpx) / 3);
|
||||
height: 0;
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
background: rgba(2, 6, 23, 0.65);
|
||||
border: 1rpx solid rgba(148, 163, 184, 0.15);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.moment-photo-img {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.moment-photo-mask {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22rpx;
|
||||
color: #94a3b8;
|
||||
background: rgba(15, 23, 42, 0.72);
|
||||
z-index: 1;
|
||||
}
|
||||
.moment-photo-remove {
|
||||
position: absolute;
|
||||
right: 6rpx;
|
||||
top: 4rpx;
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
line-height: 40rpx;
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
border-radius: 999rpx;
|
||||
z-index: 2;
|
||||
}
|
||||
.moment-photo-add {
|
||||
border: 2rpx dashed rgba(148, 163, 184, 0.35);
|
||||
background: rgba(15, 23, 42, 0.35);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.moment-photo-add-plus {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -52%);
|
||||
font-size: 64rpx;
|
||||
font-weight: 300;
|
||||
color: rgba(148, 163, 184, 0.85);
|
||||
line-height: 1;
|
||||
}
|
||||
.moment-hint {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
.submit-btn-loading { opacity: 0.72; pointer-events: none; }
|
||||
|
||||
/* 审核模式:盖住整页(含自定义导航),高于 .nav-bar 的 z-index: 10 */
|
||||
.audit-mode-mask {
|
||||
|
||||
@@ -94,6 +94,7 @@ function mapRows(rows) {
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
auditMode: false,
|
||||
list: [],
|
||||
page: 1,
|
||||
total: 0,
|
||||
@@ -103,10 +104,28 @@ Page({
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44,
|
||||
auditMode: !!app.globalData.auditMode,
|
||||
})
|
||||
try {
|
||||
app.getAuditMode && app.getAuditMode().catch(() => {})
|
||||
} catch (_) {}
|
||||
this.reload()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.setData({ auditMode: !!app.globalData.auditMode })
|
||||
},
|
||||
|
||||
goCompose() {
|
||||
if (app.globalData.auditMode) {
|
||||
wx.showToast({ title: '当前为体验版,暂无法访问', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.navigateTo({ url: '/pages/super-article-editor/super-article-editor' })
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
this.loadMore()
|
||||
},
|
||||
@@ -127,6 +146,47 @@ Page({
|
||||
wx.navigateTo({ url: `/pages/super-article-editor/super-article-editor?id=${encodeURIComponent(String(id))}` })
|
||||
},
|
||||
|
||||
tapDeleteArticle(e) {
|
||||
const id = e.currentTarget?.dataset?.id
|
||||
if (!id || this._articleDeleting) return
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.showModal({
|
||||
title: '删除动态',
|
||||
content: '删除后不可恢复,确定删除?',
|
||||
confirmText: '删除',
|
||||
confirmColor: '#dc2626',
|
||||
success: async (r) => {
|
||||
if (!r.confirm) return
|
||||
this._articleDeleting = true
|
||||
wx.showLoading({ title: '删除中…', mask: true })
|
||||
try {
|
||||
await app.request({
|
||||
url: `/api/miniprogram/super/articles/${encodeURIComponent(String(id))}?userId=${encodeURIComponent(String(userId))}`,
|
||||
method: 'DELETE',
|
||||
})
|
||||
wx.hideLoading()
|
||||
this._articleDeleting = false
|
||||
const list = this.data.list.filter((x) => String(x.id) !== String(id))
|
||||
const total = Math.max(0, (Number(this.data.total) || 0) - 1)
|
||||
this.setData({
|
||||
list,
|
||||
total,
|
||||
hasMore: list.length < total,
|
||||
})
|
||||
wx.showToast({ title: '已删除', icon: 'success' })
|
||||
} catch (_) {
|
||||
wx.hideLoading()
|
||||
this._articleDeleting = false
|
||||
/* app.request 已 toast */
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
onThumbError(e) {
|
||||
const rowIdx = Number(e.currentTarget.dataset.rowidx)
|
||||
const slotIdx = Number(e.currentTarget.dataset.slotidx)
|
||||
|
||||
@@ -8,10 +8,21 @@
|
||||
</view>
|
||||
<view class="nav-offset" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<view class="feed-wrap">
|
||||
<view
|
||||
class="compose-bar-fixed"
|
||||
wx:if="{{!auditMode}}"
|
||||
style="top: calc({{statusBarHeight}}px + 44px);"
|
||||
>
|
||||
<view class="compose-bar-btn" bindtap="goCompose">
|
||||
<text class="compose-bar-plus">+</text>
|
||||
<text>发动态</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="feed-wrap {{!auditMode ? 'feed-wrap-has-compose' : ''}}">
|
||||
<view class="empty-block" wx:if="{{!loading && list.length === 0}}">
|
||||
<text class="empty-text">暂无动态</text>
|
||||
<text class="empty-sub">去「发布文章」写一条吧</text>
|
||||
<text class="empty-sub">点击上方「发动态」写一条吧</text>
|
||||
</view>
|
||||
|
||||
<block wx:for="{{list}}" wx:for-index="rowIdx" wx:key="id">
|
||||
@@ -58,6 +69,7 @@
|
||||
data-id="{{item.id}}"
|
||||
bindtap="openEdit"
|
||||
>编辑</view>
|
||||
<view class="feed-btn danger" data-id="{{item.id}}" bindtap="tapDeleteArticle">删除</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
@@ -41,10 +41,45 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.compose-bar-fixed {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 99;
|
||||
padding: 10px 14px 12px;
|
||||
background: rgba(10, 10, 12, 0.96);
|
||||
border-bottom: 1px solid rgba(34, 211, 238, 0.12);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.compose-bar-btn {
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0a0a0c;
|
||||
background: linear-gradient(135deg, #22d3ee, #14b8a6);
|
||||
}
|
||||
|
||||
.compose-bar-plus {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.feed-wrap {
|
||||
padding: 12px 14px 28px;
|
||||
}
|
||||
|
||||
.feed-wrap-has-compose {
|
||||
padding-top: 72px;
|
||||
}
|
||||
|
||||
.empty-block {
|
||||
padding: 48px 12px;
|
||||
text-align: center;
|
||||
@@ -143,6 +178,8 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
border: 2rpx solid rgba(34, 211, 238, 0.32);
|
||||
}
|
||||
|
||||
.thumb-ph {
|
||||
@@ -213,6 +250,11 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.feed-btn.danger {
|
||||
color: rgba(248, 113, 113, 0.95);
|
||||
border: 1px solid rgba(248, 113, 113, 0.45);
|
||||
}
|
||||
|
||||
.load-more {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
|
||||
@@ -28,16 +28,53 @@ function relativeTime(iso) {
|
||||
return `${m}-${day}`
|
||||
}
|
||||
|
||||
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 : []
|
||||
}
|
||||
|
||||
/** 相对路径配图拼 API 域名,否则真机不显示 */
|
||||
function normalizeMediaSrc(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 mapFeedRows(rows) {
|
||||
return (rows || []).map((r) => {
|
||||
const nick = String(r.authorNickname || '').trim() || '超级个体'
|
||||
const av = String(r.authorAvatar || '').trim()
|
||||
const avatarOk = isSafeImageSrc(av)
|
||||
const imgs = Array.isArray(r.images) ? r.images : []
|
||||
const displayImages = imgs.map((u) => String(u || '').trim()).filter((u) => isSafeImageSrc(u))
|
||||
const avRaw = String(r.authorAvatar || '').trim()
|
||||
const av = normalizeMediaSrc(avRaw)
|
||||
const avatarOk =
|
||||
!!av &&
|
||||
(/^https?:\/\//i.test(av) || av.startsWith('wxfile://') || av.startsWith('cloud://')) &&
|
||||
isSafeImageSrc(av)
|
||||
const imgs = parseImagesField(r.images)
|
||||
const displayImages = imgs
|
||||
.map((u) => normalizeMediaSrc(String(u || '').trim()))
|
||||
.filter(
|
||||
(u) =>
|
||||
!!u &&
|
||||
(/^https?:\/\//i.test(u) || u.startsWith('wxfile://') || u.startsWith('cloud://')),
|
||||
)
|
||||
return Object.assign({}, r, {
|
||||
authorNickname: nick,
|
||||
avatarSrc: av,
|
||||
avatarSrc: avatarOk ? av : avRaw,
|
||||
avatarOk,
|
||||
avatarLetter: avatarLetter(nick),
|
||||
timeText: relativeTime(r.createdAt),
|
||||
|
||||
@@ -130,12 +130,16 @@
|
||||
.feed-img {
|
||||
width: calc((100% - 20rpx) / 3);
|
||||
height: 200rpx;
|
||||
border-radius: 0;
|
||||
border-radius: 10rpx;
|
||||
box-sizing: border-box;
|
||||
border: 2rpx solid rgba(34, 211, 238, 0.32);
|
||||
background: rgba(15, 23, 42, 0.45);
|
||||
}
|
||||
|
||||
.feed-img-one {
|
||||
width: 100%;
|
||||
height: 320rpx;
|
||||
border-radius: 14rpx;
|
||||
}
|
||||
|
||||
.feed-foot {
|
||||
|
||||
@@ -164,6 +164,53 @@ func MiniprogramSuperArticleFeed(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func deriveSuperArticleTitleFromContent(content string) string {
|
||||
content = strings.TrimSpace(content)
|
||||
if content == "" {
|
||||
return ""
|
||||
}
|
||||
parts := strings.SplitN(content, "\n", 2)
|
||||
line := strings.TrimSpace(parts[0])
|
||||
line = strings.TrimRight(line, "\r")
|
||||
if line == "" {
|
||||
return ""
|
||||
}
|
||||
rs := []rune(line)
|
||||
if len(rs) > 40 {
|
||||
return string(rs[:40])
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
// finalizeSuperArticleTitleAndBody 发动态:标题可省略(由正文首行或默认文案补齐);正文与配图至少一项。
|
||||
func finalizeSuperArticleTitleAndBody(title, content string, imageCount int) (outTitle, outBody, errMsg string) {
|
||||
title = strings.TrimSpace(title)
|
||||
content = strings.TrimSpace(content)
|
||||
if content == "" && imageCount == 0 {
|
||||
return "", "", "请填写文字或添加配图"
|
||||
}
|
||||
if len([]rune(content)) > 5000 {
|
||||
return "", "", "正文最多 5000 字"
|
||||
}
|
||||
outTitle = title
|
||||
if outTitle != "" && len([]rune(outTitle)) > 40 {
|
||||
outTitle = string([]rune(outTitle)[:40])
|
||||
}
|
||||
if outTitle == "" {
|
||||
outTitle = deriveSuperArticleTitleFromContent(content)
|
||||
}
|
||||
if outTitle == "" && imageCount > 0 {
|
||||
outTitle = "分享图片"
|
||||
}
|
||||
if outTitle == "" {
|
||||
outTitle = "动态"
|
||||
}
|
||||
if len([]rune(outTitle)) > 40 {
|
||||
outTitle = string([]rune(outTitle)[:40])
|
||||
}
|
||||
return outTitle, content, ""
|
||||
}
|
||||
|
||||
func normalizeSuperArticleImageURLs(in []string) ([]string, string) {
|
||||
if len(in) == 0 {
|
||||
return []string{}, ""
|
||||
@@ -208,16 +255,8 @@ func MiniprogramSuperArticleCreate(c *gin.Context) {
|
||||
req.UserID = strings.TrimSpace(req.UserID)
|
||||
req.Title = strings.TrimSpace(req.Title)
|
||||
req.Content = strings.TrimSpace(req.Content)
|
||||
if req.UserID == "" || req.Title == "" || req.Content == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "标题和正文不能为空"})
|
||||
return
|
||||
}
|
||||
if len([]rune(req.Title)) > 40 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "标题最多 40 字"})
|
||||
return
|
||||
}
|
||||
if len([]rune(req.Content)) > 5000 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "正文最多 5000 字"})
|
||||
if req.UserID == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -232,6 +271,13 @@ func MiniprogramSuperArticleCreate(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
title, body, verr := finalizeSuperArticleTitleAndBody(req.Title, req.Content, len(imgURLs))
|
||||
if verr != "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": verr})
|
||||
return
|
||||
}
|
||||
req.Title, req.Content = title, body
|
||||
|
||||
db := database.DB()
|
||||
if _, ok := miniprogramSuperIndividualPerson(db, req.UserID); !ok {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "仅超级个体可发文章", "forbidden": true})
|
||||
@@ -468,16 +514,8 @@ func MiniprogramSuperArticleUpdate(c *gin.Context) {
|
||||
req.UserID = strings.TrimSpace(req.UserID)
|
||||
req.Title = strings.TrimSpace(req.Title)
|
||||
req.Content = strings.TrimSpace(req.Content)
|
||||
if req.UserID == "" || req.Title == "" || req.Content == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "标题和正文不能为空"})
|
||||
return
|
||||
}
|
||||
if len([]rune(req.Title)) > 40 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "标题最多 40 字"})
|
||||
return
|
||||
}
|
||||
if len([]rune(req.Content)) > 5000 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "正文最多 5000 字"})
|
||||
if req.UserID == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -492,6 +530,13 @@ func MiniprogramSuperArticleUpdate(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
title, body, verr := finalizeSuperArticleTitleAndBody(req.Title, req.Content, len(imgURLs))
|
||||
if verr != "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": verr})
|
||||
return
|
||||
}
|
||||
req.Title, req.Content = title, body
|
||||
|
||||
db := database.DB()
|
||||
var row model.SuperArticle
|
||||
if err := db.First(&row, id).Error; err != nil {
|
||||
@@ -531,3 +576,42 @@ func MiniprogramSuperArticleUpdate(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// MiniprogramSuperArticleDelete 删除动态(作者本人 + 超级个体)。
|
||||
// DELETE /api/miniprogram/super/articles/:id?userId=
|
||||
// POST /api/miniprogram/super/articles/:id/delete?userId= (与 DELETE 等价,便于不支持 DELETE 的客户端调试)
|
||||
func MiniprogramSuperArticleDelete(c *gin.Context) {
|
||||
idRaw := strings.TrimSpace(c.Param("id"))
|
||||
id, err := strconv.ParseUint(idRaw, 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "参数错误"})
|
||||
return
|
||||
}
|
||||
userID := strings.TrimSpace(c.Query("userId"))
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 userId"})
|
||||
return
|
||||
}
|
||||
|
||||
db := database.DB()
|
||||
if _, ok := miniprogramSuperIndividualPerson(db, userID); !ok {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "仅超级个体可操作", "forbidden": true})
|
||||
return
|
||||
}
|
||||
|
||||
var row model.SuperArticle
|
||||
if err := db.First(&row, id).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "动态不存在"})
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(row.UserID) != userID {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "无权删除"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.Delete(&row).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
@@ -463,6 +463,9 @@ func Setup(cfg *config.Config) *gin.Engine {
|
||||
miniprogram.GET("/super/articles", handler.MiniprogramSuperArticleList)
|
||||
miniprogram.GET("/super/articles/:id", handler.MiniprogramSuperArticleDetail)
|
||||
miniprogram.PUT("/super/articles/:id", handler.MiniprogramSuperArticleUpdate)
|
||||
miniprogram.DELETE("/super/articles/:id", handler.MiniprogramSuperArticleDelete)
|
||||
// 与 DELETE 等价:便于本地用表单/部分网关仅放行 POST 时调试(query 仍传 userId)
|
||||
miniprogram.POST("/super/articles/:id/delete", handler.MiniprogramSuperArticleDelete)
|
||||
miniprogram.POST("/super/articles", handler.MiniprogramSuperArticleCreate)
|
||||
miniprogram.POST("/super/articles/generate", handler.MiniprogramSuperArticleGenerate)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user