/** * Soul创业派对 - 超级个体发文章(AI 生成 + 手动编辑发布) */ const app = getApp() const { uploadByApi } = require('../../utils/miniprogramUpload') const TEXT_EXTS = ['.txt', '.md', '.json', '.csv', '.log', '.yaml', '.yml'] const IMAGE_EXTS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'] const MAX_TEXT_READ_BYTES = 1024 * 1024 const MAX_REFERENCE_SEND = 28000 function newAttachId() { return `f_${Date.now()}_${Math.random().toString(36).slice(2, 9)}` } Page({ data: { statusBarHeight: 44, auditMode: false, title: '', content: '', saving: false, meNickname: '', aiPrompt: '', attachedFiles: [], generating: false, }, /** 遮挡层拦截滚动穿透 */ preventMove() {}, syncAuditMode() { const audit = !!app.globalData.auditMode this.setData({ auditMode: audit }) return audit }, onLoad() { 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) } } catch (_) { wx.showToast({ title: '身份校验失败', icon: 'none' }) setTimeout(() => wx.navigateBack(), 400) } }, onAiPromptInput(e) { this.setData({ aiPrompt: e.detail.value || '' }) }, onTitleInput(e) { this.setData({ title: e.detail.value || '' }) }, onContentInput(e) { this.setData({ content: e.detail.value || '' }) }, insertMentionSelf() { if (this.data.auditMode) return const nick = this.data.meNickname || '我' this.setData({ content: `${this.data.content}@${nick} ` }) }, insertHashLink() { if (this.data.auditMode) return this.setData({ content: `${this.data.content}#链接(https://)` }) }, /** 参考素材:仅从相册/相机选图片并上传(AI 侧按图理解) */ chooseReferenceImages() { this.syncAuditMode() if (this.data.auditMode || app.globalData.auditMode) { wx.showToast({ title: '审核模式下不可用', icon: 'none' }) return } this._pickReferenceImagesFromAlbum() }, _pickReferenceImagesFromAlbum() { 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, })) } if (typeof wx.chooseMedia === 'function') { wx.chooseMedia({ count: 9, mediaType: ['image'], sourceType: ['album', 'camera'], sizeType: ['compressed'], success: (res) => this.handlePickedFiles(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: 9, sizeType: ['compressed'], sourceType: ['album', 'camera'], success: (res) => this.handlePickedFiles(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' }) }, _pickUploadUrl(up) { if (!up || typeof up !== 'object') return '' return String(up.url || up.fullUrl || up.data?.url || up.data?.fullUrl || '').trim() }, _patchAttach(id, patch) { const next = this.data.attachedFiles.map((x) => x.id === id ? Object.assign({}, x, patch) : x, ) this.setData({ attachedFiles: next }) }, _removeAttachById(id) { this.setData({ attachedFiles: this.data.attachedFiles.filter((x) => x.id !== id), }) }, async handlePickedFiles(tempFiles) { if (!tempFiles.length) return for (const f of tempFiles) { const name = String(f.name || '未命名').trim() || '未命名' const filePath = f.path const size = f.size || 0 const dot = name.lastIndexOf('.') const ext = dot >= 0 ? name.slice(dot).toLowerCase() : '' const id = newAttachId() if (size > 29 * 1024 * 1024) { wx.showToast({ title: `${name} 超过30MB上限`, icon: 'none' }) continue } if (IMAGE_EXTS.includes(ext)) { this.setData({ attachedFiles: this.data.attachedFiles.concat([ { id, name, kind: 'image', url: '', uploading: true }, ]), }) try { const up = await uploadByApi({ filePath, folder: 'article-images' }) const url = this._pickUploadUrl(up) if (!url) throw new Error('no url') this._patchAttach(id, { url, uploading: false }) } catch (_) { this._removeAttachById(id) wx.showToast({ title: `${name} 上传失败`, icon: 'none' }) } continue } if (TEXT_EXTS.includes(ext) && size <= MAX_TEXT_READ_BYTES && size >= 0) { try { const fs = wx.getFileSystemManager() const textContent = fs.readFileSync(filePath, 'utf8') this.setData({ attachedFiles: this.data.attachedFiles.concat([ { id, name, kind: 'text', textContent, uploading: false }, ]), }) } catch (_) { await this._uploadBinaryAttach(id, name, filePath) } continue } await this._uploadBinaryAttach(id, name, filePath) } }, async _uploadBinaryAttach(id, name, filePath) { this.setData({ attachedFiles: this.data.attachedFiles.concat([ { id, name, kind: 'binary', url: '', uploading: true }, ]), }) try { const up = await uploadByApi({ filePath, folder: 'book-attachments' }) const url = this._pickUploadUrl(up) if (!url) throw new Error('no url') this._patchAttach(id, { url, uploading: false }) } catch (_) { this._removeAttachById(id) wx.showToast({ title: `${name} 上传失败`, icon: 'none' }) } }, removeAttachedFile(e) { if (this.data.auditMode) return const id = e.currentTarget.dataset.id if (!id) return this._removeAttachById(id) }, 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.attachedFiles.some((x) => x.uploading)) { wx.showToast({ title: '请等待图片上传完成', icon: 'none' }) return } const prompt = String(this.data.aiPrompt || '').trim() const parts = [] for (const f of this.data.attachedFiles) { if (f.kind === 'text' && f.textContent) { parts.push(`--- ${f.name} ---\n${f.textContent}`) } } let referenceText = parts.join('\n\n').trim() if (referenceText.length > MAX_REFERENCE_SEND) { referenceText = referenceText.slice(0, MAX_REFERENCE_SEND) + '\n\n(本地参考文末已截断)' } const materialUrls = this.data.attachedFiles .filter((x) => x.kind === 'binary' && x.url) .map((x) => x.url) const imageUrls = this.data.attachedFiles .filter((x) => x.kind === 'image' && x.url) .map((x) => x.url) if (!prompt && !referenceText && materialUrls.length === 0 && 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() if (!title || !content) throw new Error('生成结果不完整') this.setData({ title, content }) 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 const title = String(this.data.title || '').trim() const content = String(this.data.content || '').trim() if (!title || !content) { wx.showToast({ title: '请填写标题和正文', icon: 'none' }) return } this.setData({ saving: true }) try { const res = await app.request({ url: '/api/miniprogram/super/articles', method: 'POST', data: { userId, title, content }, }) if (!res?.success) throw new Error(res?.error || '发布失败') wx.showToast({ title: '发布成功', icon: 'success' }) setTimeout(() => wx.navigateBack(), 400) } catch (e) { wx.showToast({ title: e.message || '发布失败', icon: 'none' }) } finally { this.setData({ saving: false }) } }, goBack() { wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/my/my' }) }) }, })