- 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.
546 lines
17 KiB
JavaScript
546 lines
17 KiB
JavaScript
/**
|
||
* 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: {
|
||
/** 临时隐藏「AI 写文章」卡片;需上线时再设为 true */
|
||
showAiWriteSection: false,
|
||
statusBarHeight: 44,
|
||
auditMode: false,
|
||
title: '',
|
||
content: '',
|
||
bodyImages: [],
|
||
saving: false,
|
||
meNickname: '',
|
||
aiPrompt: '',
|
||
attachedFiles: [],
|
||
generating: false,
|
||
editArticleId: '',
|
||
},
|
||
|
||
/** 遮挡层拦截滚动穿透 */
|
||
preventMove() {},
|
||
|
||
syncAuditMode() {
|
||
const audit = !!app.globalData.auditMode
|
||
this.setData({ auditMode: audit })
|
||
return audit
|
||
},
|
||
|
||
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,
|
||
})
|
||
} 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) {
|
||
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://)` })
|
||
},
|
||
|
||
/** 编辑区配图(发布后展示在正文前),最多 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),
|
||
})
|
||
},
|
||
|
||
/** 参考素材:仅从相册/相机选图片并上传(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()
|
||
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) || '生成失败',
|
||
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) {
|
||
wx.showToast({ title: '已提交审核', icon: 'success' })
|
||
setTimeout(() => wx.navigateBack(), 450)
|
||
} else {
|
||
this.setData({ title: '', content: '', bodyImages: [] })
|
||
wx.showToast({ title: '已提交审核', icon: 'success' })
|
||
}
|
||
} 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' }) })
|
||
},
|
||
})
|