From 0cc8e2b270563682ff37d3e734f055a23ebae434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=98=E9=A3=8E?= Date: Sat, 9 May 2026 18:15:45 +0800 Subject: [PATCH] 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. --- .../pages/member-detail/member-detail.js | 133 ++++++++++- .../pages/member-detail/member-detail.wxml | 219 +++++++++++------- .../pages/member-detail/member-detail.wxss | 90 +++++++ miniprogram/pages/my/my.js | 11 +- miniprogram/pages/my/my.wxml | 18 +- miniprogram/pages/purchases/purchases.js | 169 +++++++++++--- miniprogram/pages/purchases/purchases.wxml | 32 +-- miniprogram/pages/purchases/purchases.wxss | 47 +++- .../super-article-detail.js | 4 +- .../super-article-detail.wxml | 2 +- .../super-article-detail.wxss | 2 + .../super-article-editor.js | 34 +-- .../super-article-editor.wxml | 66 +++--- .../super-article-editor.wxss | 135 ++++++++++- .../super-article-mine/super-article-mine.js | 62 ++++- .../super-article-mine.wxml | 16 +- .../super-article-mine.wxss | 42 ++++ .../pages/super-moments/super-moments.js | 47 +++- .../pages/super-moments/super-moments.wxss | 6 +- .../handler/miniprogram_super_articles.go | 124 ++++++++-- soul-api/internal/router/router.go | 3 + 21 files changed, 1013 insertions(+), 249 deletions(-) diff --git a/miniprogram/pages/member-detail/member-detail.js b/miniprogram/pages/member-detail/member-detail.js index 3eda1f11..a93673a7 100644 --- a/miniprogram/pages/member-detail/member-detail.js +++ b/miniprogram/pages/member-detail/member-detail.js @@ -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() }, diff --git a/miniprogram/pages/member-detail/member-detail.wxml b/miniprogram/pages/member-detail/member-detail.wxml index 2b99413c..f87bff84 100644 --- a/miniprogram/pages/member-detail/member-detail.wxml +++ b/miniprogram/pages/member-detail/member-detail.wxml @@ -110,112 +110,163 @@ - - - - {{isOwnProfile ? '我的文章' : 'TA文章'}} - {{articleList.length}} 篇 - - - - {{item.auditLabel}} - {{item.title}} - {{item.preview || item.content}} + + + + + {{isOwnProfile ? '我的档案' : 'TA档案'}} + + + {{isOwnProfile ? '我的动态' : 'TA动态'}} + {{articleList.length}} - - - - - - - + + + + 职业画像 - + + + 行业 + {{member.industry}} + + + 职位 + {{member.position}} + + + 业务体量 + {{member.businessScale}} - - - 行业 - {{member.industry}} + + + + + + 我擅长 - - 职位 - {{member.position}} + + {{member.skills}} - - 业务体量 - {{member.businessScale}} + + + + + + + 个人故事 + + 最赚钱的一个月 + {{member.bestMonth}} + + + + 最有成就感的事 + {{member.achievement}} + + + + 人生的转折点 + {{member.turningPoint}} + + + + + + + + 互动需求 + + + + 我能帮你 + {{member.canHelp}} + + + 我需要 + {{member.needHelp}} + + + + + + 暂未填写档案资料 - - - - - - 我擅长 - - - {{member.skills}} - - - - - - - - - 个人故事 - - - 最赚钱的一个月 - {{member.bestMonth}} - - - - 最有成就感的事 - {{member.achievement}} - - - - 人生的转折点 - {{member.turningPoint}} - - - - - - - - - 互助需求 - - - - 我能帮你 - {{member.canHelp}} - - - 我需要 - {{member.needHelp}} + + 加载中… + + + {{item.auditLabel}} + {{item.title}} + {{item.preview || item.content}} + + + + + + + {{th.loadFailed ? '加载失败' : '无效'}} + + + + +{{item.thumbMoreCount}} + + + 暂无动态 + - - - + + + 项目介绍 {{member.project}} - diff --git a/miniprogram/pages/member-detail/member-detail.wxss b/miniprogram/pages/member-detail/member-detail.wxss index 6a42e1f4..58deecb8 100644 --- a/miniprogram/pages/member-detail/member-detail.wxss +++ b/miniprogram/pages/member-detail/member-detail.wxss @@ -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; diff --git a/miniprogram/pages/my/my.js b/miniprogram/pages/my/my.js index e9b86fa3..e31a87d8 100644 --- a/miniprogram/pages/my/my.js +++ b/miniprogram/pages/my/my.js @@ -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' } diff --git a/miniprogram/pages/my/my.wxml b/miniprogram/pages/my/my.wxml index 3fe37642..af10aaf5 100644 --- a/miniprogram/pages/my/my.wxml +++ b/miniprogram/pages/my/my.wxml @@ -89,7 +89,7 @@ - + @@ -100,13 +100,7 @@ - 我的订单 - - - - - - 我的代付 + 订单与代付 @@ -120,12 +114,6 @@ 链接轨迹 - - - - - 发布文章 - @@ -135,7 +123,7 @@ - + diff --git a/miniprogram/pages/purchases/purchases.js b/miniprogram/pages/purchases/purchases.js index 441d9cd7..34a95599 100644 --- a/miniprogram/pages/purchases/purchases.js +++ b/miniprogram/pages/purchases/purchases.js @@ -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}` : '' } } }) diff --git a/miniprogram/pages/purchases/purchases.wxml b/miniprogram/pages/purchases/purchases.wxml index 7211d90b..2e2343c3 100644 --- a/miniprogram/pages/purchases/purchases.wxml +++ b/miniprogram/pages/purchases/purchases.wxml @@ -2,7 +2,7 @@ - 我的订单 + 订单与代付 @@ -19,21 +19,23 @@ - - - {{index + 1}} - - {{item.title}} - {{item.subLine}} + + + + {{item.kind === 'giftPay' ? '代付' : '订单'}} + + {{item.title}} + {{item.subLine}} + + {{item.actionLabel}} + + + 取消 + + + 分享 - {{item.actionLabel}} - 暂无订单 + 暂无订单与代付记录 diff --git a/miniprogram/pages/purchases/purchases.wxss b/miniprogram/pages/purchases/purchases.wxss index 9344d2c1..45d60813 100644 --- a/miniprogram/pages/purchases/purchases.wxss +++ b/miniprogram/pages/purchases/purchases.wxss @@ -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; } diff --git a/miniprogram/pages/super-article-detail/super-article-detail.js b/miniprogram/pages/super-article-detail/super-article-detail.js index 3b44abb3..22ec506a 100644 --- a/miniprogram/pages/super-article-detail/super-article-detail.js +++ b/miniprogram/pages/super-article-detail/super-article-detail.js @@ -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))}` : '', } }, diff --git a/miniprogram/pages/super-article-detail/super-article-detail.wxml b/miniprogram/pages/super-article-detail/super-article-detail.wxml index 547442d6..82d0ee04 100644 --- a/miniprogram/pages/super-article-detail/super-article-detail.wxml +++ b/miniprogram/pages/super-article-detail/super-article-detail.wxml @@ -3,7 +3,7 @@ - 文章详情 + 动态详情 diff --git a/miniprogram/pages/super-article-detail/super-article-detail.wxss b/miniprogram/pages/super-article-detail/super-article-detail.wxss index bb3043dc..1f03f75e 100644 --- a/miniprogram/pages/super-article-detail/super-article-detail.wxss +++ b/miniprogram/pages/super-article-detail/super-article-detail.wxss @@ -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 { diff --git a/miniprogram/pages/super-article-editor/super-article-editor.js b/miniprogram/pages/super-article-editor/super-article-editor.js index e26b9e50..2c918379 100644 --- a/miniprogram/pages/super-article-editor/super-article-editor.js +++ b/miniprogram/pages/super-article-editor/super-article-editor.js @@ -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 || '发布失败') diff --git a/miniprogram/pages/super-article-editor/super-article-editor.wxml b/miniprogram/pages/super-article-editor/super-article-editor.wxml index e10107f9..2ac65f5d 100644 --- a/miniprogram/pages/super-article-editor/super-article-editor.wxml +++ b/miniprogram/pages/super-article-editor/super-article-editor.wxml @@ -3,14 +3,14 @@ - {{editArticleId ? '编辑动态' : '发文章'}} + {{editArticleId ? '编辑动态' : '发动态'}} - AI 写文章 + AI 帮你写 参考图片(可多选) + 添加图片 @@ -29,43 +29,49 @@ 提示词 - + - {{generating ? '生成中…' : '生成文章'}} + {{generating ? '生成中…' : '生成'}} - - 编辑与发布 - 标题 - - - - - 配图(可选,最多 9 张) - - + 添加配图 - - - - - 上传中… - × - - - 配图会展示在正文上方(与书籍正文分离存储);单张建议不超 30MB。 - - 正文 + + @自己 - #超链接 + #链接 - - + + - 提示:可直接输入 @昵称 或 #链接(https://xxx) - {{saving ? (editArticleId ? '提交中…' : '发布中…') : (editArticleId ? '提交审核' : '发布文章')}} + + + + 上传中… + × + + + + + + + 配图 {{bodyImages.length}}/9 · 展示在文字下方 · 单张建议不超 30MB · 可直接输入 @昵称 或 #链接(https://xxx) + + + + + + {{saving ? (editArticleId ? '提交中…' : '发布中…') : (editArticleId ? '提交审核' : '发布')}} + diff --git a/miniprogram/pages/super-article-editor/super-article-editor.wxss b/miniprogram/pages/super-article-editor/super-article-editor.wxss index eb79b77f..8c349ec4 100644 --- a/miniprogram/pages/super-article-editor/super-article-editor.wxss +++ b/miniprogram/pages/super-article-editor/super-article-editor.wxss @@ -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 { diff --git a/miniprogram/pages/super-article-mine/super-article-mine.js b/miniprogram/pages/super-article-mine/super-article-mine.js index 16de3b7f..63605359 100644 --- a/miniprogram/pages/super-article-mine/super-article-mine.js +++ b/miniprogram/pages/super-article-mine/super-article-mine.js @@ -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) diff --git a/miniprogram/pages/super-article-mine/super-article-mine.wxml b/miniprogram/pages/super-article-mine/super-article-mine.wxml index a725708d..79d8ef8f 100644 --- a/miniprogram/pages/super-article-mine/super-article-mine.wxml +++ b/miniprogram/pages/super-article-mine/super-article-mine.wxml @@ -8,10 +8,21 @@ - + + + + 发动态 + + + + 暂无动态 - 去「发布文章」写一条吧 + 点击上方「发动态」写一条吧 @@ -58,6 +69,7 @@ data-id="{{item.id}}" bindtap="openEdit" >编辑 + 删除 diff --git a/miniprogram/pages/super-article-mine/super-article-mine.wxss b/miniprogram/pages/super-article-mine/super-article-mine.wxss index c6cc3763..96c25f85 100644 --- a/miniprogram/pages/super-article-mine/super-article-mine.wxss +++ b/miniprogram/pages/super-article-mine/super-article-mine.wxss @@ -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; diff --git a/miniprogram/pages/super-moments/super-moments.js b/miniprogram/pages/super-moments/super-moments.js index 14db3178..b8056171 100644 --- a/miniprogram/pages/super-moments/super-moments.js +++ b/miniprogram/pages/super-moments/super-moments.js @@ -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), diff --git a/miniprogram/pages/super-moments/super-moments.wxss b/miniprogram/pages/super-moments/super-moments.wxss index 3a380e32..30ee52de 100644 --- a/miniprogram/pages/super-moments/super-moments.wxss +++ b/miniprogram/pages/super-moments/super-moments.wxss @@ -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 { diff --git a/soul-api/internal/handler/miniprogram_super_articles.go b/soul-api/internal/handler/miniprogram_super_articles.go index 6cce95ef..a660c5b3 100644 --- a/soul-api/internal/handler/miniprogram_super_articles.go +++ b/soul-api/internal/handler/miniprogram_super_articles.go @@ -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}) +} diff --git a/soul-api/internal/router/router.go b/soul-api/internal/router/router.go index adfedf31..0470f322 100644 --- a/soul-api/internal/router/router.go +++ b/soul-api/internal/router/router.go @@ -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) }