- Introduced a utility function to ensure absolute media URLs for user profile images. - Updated the user cache synchronization method to handle profile data more effectively, including avatar, nickname, phone, and MBTI. - Refactored the avatar upload process to utilize the new URL handling function, improving reliability. - Modified the super article editor to focus on image uploads instead of file uploads, enhancing user experience with clearer labeling and prompts. This update aims to streamline user profile management and improve the image upload experience in the application.
358 lines
11 KiB
JavaScript
358 lines
11 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: {
|
||
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' }) })
|
||
},
|
||
})
|