/** * Soul创业派对 - 超级个体发动态(AI 辅助 + 朋友圈式编辑) */ const app = getApp() const { uploadByApi } = require('../../utils/miniprogramUpload') function newAttachId() { return `f_${Date.now()}_${Math.random().toString(36).slice(2, 9)}` } Page({ data: { /** 「AI 帮你写」入口(弹窗内填写提示词并生成) */ showAiWriteSection: true, aiAssistVisible: false, statusBarHeight: 44, auditMode: false, title: '', content: '', bodyImages: [], saving: false, meNickname: '', aiPrompt: '', generating: false, editArticleId: '', /** 正文光标,用于 @自己 插入 */ contentCursor: 0, }, /** 遮挡层拦截滚动穿透 */ preventMove() {}, syncAuditMode() { const audit = !!app.globalData.auditMode const patch = { auditMode: audit } if (audit) patch.aiAssistVisible = false this.setData(patch) return audit }, openAiAssist() { this.syncAuditMode() if (this.data.auditMode || app.globalData.auditMode) { wx.showToast({ title: '审核模式下不可用', icon: 'none' }) return } if (!this.data.showAiWriteSection) return this.setData({ aiAssistVisible: true }) }, closeAiAssist() { if (this.data.generating) { wx.showToast({ title: '生成中,请稍候…', icon: 'none' }) return } this.setData({ aiAssistVisible: false }) }, /** 阻止弹层内容区点击冒泡到遮罩(部分机型兼容) */ noopModalTap() {}, onLoad(options) { const editId = options && options.id != null ? String(options.id).trim() : '' if (editId) { this._pendingEditId = editId this.setData({ editArticleId: editId }) } this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44, meNickname: String(app.globalData.userInfo?.nickname || '我').trim() || '我', }) this.syncAuditMode() try { app.getAuditMode && app.getAuditMode().catch(() => {}) } catch (_) {} this.ensureSuperIdentity() }, onShow() { this.syncAuditMode() try { app.getAuditMode && app.getAuditMode().catch(() => {}) } catch (_) {} }, async ensureSuperIdentity() { const userId = app.globalData.userInfo?.id if (!app.globalData.isLoggedIn || !userId) { wx.showToast({ title: '请先登录', icon: 'none' }) setTimeout(() => wx.navigateBack(), 400) return } try { const res = await app.request({ url: `/api/miniprogram/my/super-stats?userId=${encodeURIComponent(userId)}`, silent: true, }) if (!(res?.success && res.data?.isSuperIndividual === true)) { wx.showToast({ title: '仅超级个体可发动态', icon: 'none' }) setTimeout(() => wx.navigateBack(), 400) return } const pid = this._pendingEditId ? String(this._pendingEditId).trim() : '' if (pid) { this._pendingEditId = '' await this.loadArticleForEdit(pid) } } catch (_) { wx.showToast({ title: '身份校验失败', icon: 'none' }) setTimeout(() => wx.navigateBack(), 400) } }, async loadArticleForEdit(id) { const userId = app.globalData.userInfo?.id if (!userId) return wx.showLoading({ title: '加载中…', mask: true }) try { const res = await app.request({ url: `/api/miniprogram/super/articles/${encodeURIComponent(String(id))}?viewerUserId=${encodeURIComponent(String(userId))}`, silent: true, }) const article = res?.data if (!article) throw new Error('加载失败') if (String(article.userId) !== String(userId)) throw new Error('无权编辑') const st = String(article.auditStatus || '').toLowerCase() if (st !== 'pending' && st !== 'rejected') throw new Error('仅待审核或已驳回的动态可编辑') const imgs = Array.isArray(article.images) ? article.images.map((u) => String(u || '').trim()).filter(Boolean) : [] const bodyImages = imgs.map((url) => ({ id: newAttachId(), url, uploading: false })) this.setData({ editArticleId: String(id), title: '', content: String(article.content || ''), bodyImages, contentCursor: String(article.content || '').length, }) } catch (e) { wx.showToast({ title: (e && e.message) || '加载失败', icon: 'none' }) setTimeout(() => wx.navigateBack(), 500) } finally { try { wx.hideLoading() } catch (_) {} } }, onAiPromptInput(e) { this.setData({ aiPrompt: e.detail.value || '' }) }, onContentInput(e) { const v = e.detail.value != null ? String(e.detail.value) : '' let cursor = e.detail.cursor if (typeof cursor !== 'number' || Number.isNaN(cursor)) cursor = v.length cursor = Math.max(0, Math.min(cursor, v.length)) this.setData({ contentCursor: cursor }) }, onContentBlur(e) { const v = e.detail.value != null ? String(e.detail.value) : '' let cursor = e.detail.cursor if (typeof cursor !== 'number' || Number.isNaN(cursor)) cursor = v.length cursor = Math.max(0, Math.min(cursor, v.length)) this.setData({ contentCursor: cursor }) }, insertAtContentCursor(insert) { if (this.data.auditMode) return false const raw = this.data.content != null ? String(this.data.content) : '' const seg = String(insert || '') if (!seg) return false let pos = this.data.contentCursor if (typeof pos !== 'number' || Number.isNaN(pos)) pos = raw.length pos = Math.max(0, Math.min(pos, raw.length)) const next = raw.slice(0, pos) + seg + raw.slice(pos) const newCursor = pos + seg.length this.setData({ content: next, contentCursor: newCursor, }) return true }, insertMentionSelf() { if (this.data.auditMode) return const nick = this.data.meNickname || '我' this.insertAtContentCursor(`@${nick} `) }, /** 动态配图(发动态展示 + AI 生成多模态共用),最多 9 张 */ chooseArticleImages() { this.syncAuditMode() if (this.data.auditMode || app.globalData.auditMode) { wx.showToast({ title: '审核模式下不可用', icon: 'none' }) return } const max = 9 const cur = this.data.bodyImages.length if (cur >= max) { wx.showToast({ title: '最多 9 张配图', icon: 'none' }) return } const pickCount = max - cur const mapFromChooseMedia = (res) => { const files = res.tempFiles || [] return files.map((f, i) => ({ path: f.tempFilePath, name: `配图_${Date.now()}_${i}.jpg`, size: f.size || 0, })) } const mapFromChooseImage = (res) => { const paths = res.tempFilePaths || [] const t = Date.now() return paths.map((path, i) => ({ path, name: `配图_${t}_${i}.jpg`, size: 0, })) } const runUpload = async (tempFiles) => { for (const f of tempFiles) { if (this.data.bodyImages.length >= max) break await this._uploadOneArticleBodyImage(f) } } if (typeof wx.chooseMedia === 'function') { wx.chooseMedia({ count: pickCount, mediaType: ['image'], sourceType: ['album', 'camera'], sizeType: ['compressed'], success: (res) => { void runUpload(mapFromChooseMedia(res)) }, fail: (err) => { const em = String((err && err.errMsg) || '') if (/cancel|取消/i.test(em)) return wx.showToast({ title: '选择图片失败', icon: 'none' }) }, }) return } if (typeof wx.chooseImage === 'function') { wx.chooseImage({ count: pickCount, sizeType: ['compressed'], sourceType: ['album', 'camera'], success: (res) => { void runUpload(mapFromChooseImage(res)) }, fail: (err) => { const em = String((err && err.errMsg) || '') if (/cancel|取消/i.test(em)) return wx.showToast({ title: '选择图片失败', icon: 'none' }) }, }) return } wx.showToast({ title: '当前环境不支持选图片', icon: 'none' }) }, async _uploadOneArticleBodyImage(f) { const filePath = f.path const name = String(f.name || '配图').trim() || '配图' const size = f.size || 0 if (!filePath) return if (size > 29 * 1024 * 1024) { wx.showToast({ title: '单张请勿超过 30MB', icon: 'none' }) return } const id = newAttachId() this.setData({ bodyImages: this.data.bodyImages.concat([{ id, url: '', uploading: true }]), }) try { const up = await uploadByApi({ filePath, folder: 'super-article-images' }) const url = this._pickUploadUrl(up) if (!url) throw new Error('no url') const next = this.data.bodyImages.map((x) => x.id === id ? Object.assign({}, x, { url, uploading: false }) : x, ) this.setData({ bodyImages: next }) } catch (_) { this.setData({ bodyImages: this.data.bodyImages.filter((x) => x.id !== id), }) wx.showToast({ title: `${name} 上传失败`, icon: 'none' }) } }, removeArticleImage(e) { if (this.data.auditMode) return const id = e.currentTarget.dataset.id if (!id) return this.setData({ bodyImages: this.data.bodyImages.filter((x) => x.id !== id), }) }, _pickUploadUrl(up) { if (!up || typeof up !== 'object') return '' return String(up.url || up.fullUrl || up.data?.url || up.data?.fullUrl || '').trim() }, async generateArticle() { if (this.data.auditMode || app.globalData.auditMode) return if (this.data.generating) return const userId = app.globalData.userInfo?.id if (!userId) { wx.showToast({ title: '请先登录', icon: 'none' }) return } if (this.data.bodyImages.some((x) => x.uploading)) { wx.showToast({ title: '请等待配图上传完成', icon: 'none' }) return } const prompt = String(this.data.aiPrompt || '').trim() const referenceText = '' const materialUrls = [] const imageUrls = this.data.bodyImages .map((x) => String(x.url || '').trim()) .filter(Boolean) if (!prompt && imageUrls.length === 0) { wx.showToast({ title: '请填写提示词或添加配图', icon: 'none' }) return } this.setData({ generating: true }) try { const res = await app.request({ url: '/api/miniprogram/super/articles/generate', method: 'POST', timeout: 120000, data: { userId, description: prompt, referenceText, materialUrls, imageUrls, }, }) if (!res?.success) throw new Error(res?.error || '生成失败') const title = String(res.data?.title || '').trim() const content = String(res.data?.content || '').trim() let merged = content if (title && merged) merged = `${title}\n\n${merged}` else if (title) merged = title if (!merged.trim()) throw new Error('生成结果不完整') const mergedStr = merged.trim() this.setData({ title: '', content: mergedStr, contentCursor: mergedStr.length, aiAssistVisible: false, }) wx.showToast({ title: '已填入正文', icon: 'success' }) } catch (e) { wx.showToast({ title: (e && e.message) || '生成失败', icon: 'none', duration: 2800, }) } finally { this.setData({ generating: false }) } }, async submitArticle() { if (this.data.auditMode || app.globalData.auditMode) return if (this.data.saving) return const userId = app.globalData.userInfo?.id 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 } 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 }) try { let res if (editId) { res = await app.request({ url: `/api/miniprogram/super/articles/${encodeURIComponent(editId)}`, method: 'PUT', data: { userId, title: '', content, images }, }) } else { res = await app.request({ url: '/api/miniprogram/super/articles', method: 'POST', data: { userId, title: '', content, images }, }) } if (!res?.success) throw new Error(res?.error || '发布失败') if (!editId) { this.setData({ title: '', content: '', bodyImages: [] }) } wx.showToast({ title: '已提交审核', icon: 'success' }) setTimeout(() => { wx.redirectTo({ url: '/pages/super-article-mine/super-article-mine' }) }, 450) } catch (e) { wx.showToast({ title: e.message || '发布失败', icon: 'none' }) } finally { try { wx.hideLoading() } catch (_) {} this.setData({ saving: false }) } }, goBack() { wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/my/my' }) }) }, })