feat: update API base URLs and improve UI elements

- Changed API base URLs in app.js to point to the production server for deployment.
- Updated the my page UI by refining labels and removing unnecessary text for a cleaner look.
- Adjusted CSS styles to enhance layout and reduce whitespace, improving overall user experience.

This update aims to ensure proper API connectivity and enhance the visual presentation of the user interface.
This commit is contained in:
乘风
2026-05-09 17:26:55 +08:00
parent b1db1efeb8
commit c84c53515e
26 changed files with 1230 additions and 206 deletions

View File

@@ -7,6 +7,7 @@
"pages/index/index",
"pages/chapters/chapters",
"pages/match/match",
"pages/super-moments/super-moments",
"pages/my/my",
"pages/read/read",
"pages/link-preview/link-preview",
@@ -64,6 +65,10 @@
"pagePath": "pages/match/match",
"text": "找伙伴"
},
{
"pagePath": "pages/super-moments/super-moments",
"text": "动态"
},
{
"pagePath": "pages/my/my",
"text": "我的"

View File

@@ -1,157 +1,181 @@
/**
* Soul创业实验 - 自定义TabBar组件
* 根据后台配置动态显示/隐藏"找伙伴"按钮
* Soul创业实验 - 自定义 TabBar
* 支持:首页 / 目录 / [找伙伴] / [动态,审核模式隐藏] / 我的
*/
console.log('[TabBar] ===== 组件文件开始加载 =====')
const app = getApp()
console.log('[TabBar] App 对象:', app)
Component({
data: {
selected: 0,
color: '#8e8e93',
selectedColor: '#00CED1',
matchEnabled: false, // 找伙伴功能开关,默认关闭
list: [
matchEnabled: false,
tabList: [
{
pagePath: '/pages/index/index',
text: '首页',
iconType: 'home'
iconPath: '/assets/icons/home.svg',
isSpecial: false,
},
{
pagePath: '/pages/chapters/chapters',
text: '目录',
iconType: 'list'
iconPath: '/assets/icons/list.svg',
isSpecial: false,
},
{
pagePath: '/pages/match/match',
text: '找伙伴',
iconType: 'match',
isSpecial: true
pagePath: '/pages/super-moments/super-moments',
text: '动态',
iconPath: '/assets/icons/sparkles.svg',
isSpecial: false,
},
{
pagePath: '/pages/my/my',
text: '我的',
iconType: 'user'
}
]
iconPath: '/assets/icons/user.svg',
isSpecial: false,
},
],
},
lifetimes: {
attached() {
console.log('[TabBar] Component attached 生命周期触发')
this.loadFeatureConfig()
},
ready() {
console.log('[TabBar] Component ready 生命周期触发')
// 如果 attached 中没有成功加载,在 ready 中再次尝试
if (this.data.matchEnabled === undefined || this.data.matchEnabled === null) {
console.log('[TabBar] 在 ready 中重新加载配置')
this.loadFeatureConfig()
}
}
},
// 页面加载时也调用(兼容性更好)
attached() {
console.log('[TabBar] attached() 方法触发')
this.loadFeatureConfig()
},
},
methods: {
// 加载功能配置
buildTabList(matchEnabled, tabUi, auditMode) {
const audit = !!auditMode
const ui = tabUi || {}
const h = ui.home ? String(ui.home) : '首页'
const ch = ui.chapters ? String(ui.chapters) : '目录'
const ma = ui.match ? String(ui.match) : '找伙伴'
const mo = ui.moments ? String(ui.moments) : ui.feed ? String(ui.feed) : '动态'
const my = ui.my ? String(ui.my) : '我的'
const tabs = [
{
pagePath: '/pages/index/index',
text: h,
iconPath: '/assets/icons/home.svg',
isSpecial: false,
},
{
pagePath: '/pages/chapters/chapters',
text: ch,
iconPath: '/assets/icons/list.svg',
isSpecial: false,
},
]
if (matchEnabled) {
tabs.push({
pagePath: '/pages/match/match',
text: ma,
iconPath: '/assets/icons/partners.svg',
isSpecial: true,
})
}
if (!audit) {
tabs.push({
pagePath: '/pages/super-moments/super-moments',
text: mo,
iconPath: '/assets/icons/sparkles.svg',
isSpecial: false,
})
}
tabs.push({
pagePath: '/pages/my/my',
text: my,
iconPath: '/assets/icons/user.svg',
isSpecial: false,
})
return tabs
},
async loadFeatureConfig() {
try {
console.log('[TabBar] 开始加载功能配置...')
console.log('[TabBar] API地址:', app.globalData.baseUrl + '/api/miniprogram/config')
const res = await app.getConfig()
// 兼容两种返回格式
let matchEnabled = false
if (res && res.success && res.features) {
console.log('[TabBar] features配置:', JSON.stringify(res.features))
matchEnabled = res.features.matchEnabled === true
console.log('[TabBar] matchEnabled值:', matchEnabled)
} else if (res && res.configs && res.configs.feature_config) {
// 备用格式:从 configs.feature_config 读取
console.log('[TabBar] 使用备用格式从configs读取')
matchEnabled = res.configs.feature_config.matchEnabled === true
console.log('[TabBar] matchEnabled值:', matchEnabled)
} else {
console.log('[TabBar] ⚠️ 未找到features配置使用默认值false')
console.log('[TabBar] res对象keys:', Object.keys(res || {}))
}
const tabUi = app.globalData.configCache?.mpConfig?.mpUi?.tabBar || {}
const list = [...this.data.list]
if (tabUi.home) list[0] = { ...list[0], text: String(tabUi.home) }
if (tabUi.chapters) list[1] = { ...list[1], text: String(tabUi.chapters) }
if (tabUi.match) list[2] = { ...list[2], text: String(tabUi.match) }
if (tabUi.my) list[3] = { ...list[3], text: String(tabUi.my) }
this.setData({ matchEnabled, list }, () => {
console.log('[TabBar] ✅ matchEnabled已设置为:', this.data.matchEnabled)
// 配置加载完成后,根据当前路由设置选中状态
let matchEnabled = false
if (res && res.success && res.features) {
matchEnabled = res.features.matchEnabled === true
} else if (res && res.configs && res.configs.feature_config) {
matchEnabled = res.configs.feature_config.matchEnabled === true
}
const tabUi = app.globalData.configCache?.mpConfig?.mpUi?.tabBar || {}
try {
await app.getAuditMode()
} catch (_) {}
const auditMode = !!app.globalData.auditMode
const tabList = this.buildTabList(matchEnabled, tabUi, auditMode)
this.setData({ matchEnabled, tabList }, () => {
this.updateSelected()
})
// 如果当前在找伙伴页面,但功能已关闭,跳转到首页
if (!matchEnabled) {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
if (currentPage && currentPage.route === 'pages/match/match') {
console.log('[TabBar] 找伙伴功能已关闭从match页面跳转到首页')
wx.switchTab({ url: '/pages/index/index' })
}
}
} catch (error) {
console.log('[TabBar] ❌ 加载功能配置失败:', error)
console.log('[TabBar] 错误详情:', error.message || error)
// 默认关闭找伙伴功能
this.setData({ matchEnabled: false }, () => {
if (auditMode) {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
if (currentPage && currentPage.route === 'pages/super-moments/super-moments') {
wx.switchTab({ url: '/pages/index/index' })
}
}
} catch (_) {
try {
await app.getAuditMode()
} catch (__) {}
const auditMode = !!app.globalData.auditMode
const tabList = this.buildTabList(false, {}, auditMode)
this.setData({ matchEnabled: false, tabList }, () => {
this.updateSelected()
})
if (auditMode) {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
if (currentPage && currentPage.route === 'pages/super-moments/super-moments') {
wx.switchTab({ url: '/pages/index/index' })
}
}
}
},
// 根据当前路由更新选中状态
updateSelected() {
const pages = getCurrentPages()
if (pages.length === 0) return
const currentPage = pages[pages.length - 1]
const route = currentPage.route
const route = pages[pages.length - 1].route
const tabList = this.data.tabList || []
let selected = 0
const { matchEnabled } = this.data
// 根据路由匹配对应的索引
if (route === 'pages/index/index') {
selected = 0
} else if (route === 'pages/chapters/chapters') {
selected = 1
} else if (route === 'pages/match/match') {
selected = 2
} else if (route === 'pages/my/my') {
selected = matchEnabled ? 3 : 2
for (let i = 0; i < tabList.length; i++) {
const p = tabList[i].pagePath.replace(/^\//, '')
if (route === p) {
selected = i
break
}
}
this.setData({ selected })
},
switchTab(e) {
const data = e.currentTarget.dataset
const url = data.path
const index = data.index
const url = e.currentTarget.dataset.path
const index = Number(e.currentTarget.dataset.index)
if (Number.isNaN(index)) return
if (this.data.selected === index) return
wx.switchTab({ url })
}
}
},
},
})

View File

@@ -1,47 +1,36 @@
<!--custom-tab-bar/index.wxml-->
<view class="tab-bar {{matchEnabled ? 'tab-bar-four' : 'tab-bar-three'}}">
<view class="tab-bar" wx:if="{{tabList.length}}">
<view class="tab-bar-border"></view>
<!-- 首页 -->
<view class="tab-bar-item" data-path="{{list[0].pagePath}}" data-index="0" bindtap="switchTab">
<view class="icon-wrapper">
<image class="tab-icon {{selected === 0 ? 'icon-active' : ''}}"
src="/assets/icons/home.svg"
mode="aspectFit"
style="color: {{selected === 0 ? selectedColor : color}}"></image>
</view>
<view class="tab-bar-text" style="color: {{selected === 0 ? selectedColor : color}}">{{list[0].text}}</view>
</view>
<!-- 目录 -->
<view class="tab-bar-item" data-path="{{list[1].pagePath}}" data-index="1" bindtap="switchTab">
<view class="icon-wrapper">
<image class="tab-icon {{selected === 1 ? 'icon-active' : ''}}"
src="/assets/icons/list.svg"
mode="aspectFit"
style="color: {{selected === 1 ? selectedColor : color}}"></image>
<block wx:for="{{tabList}}" wx:key="pagePath">
<view
wx:if="{{item.isSpecial}}"
class="tab-bar-item special-item"
data-path="{{item.pagePath}}"
data-index="{{index}}"
bindtap="switchTab"
>
<view class="special-button {{selected === index ? 'special-active' : ''}}">
<image class="special-icon" src="{{item.iconPath}}" mode="aspectFit"></image>
</view>
<view class="tab-bar-text special-text" style="color: {{selected === index ? selectedColor : color}}">{{item.text}}</view>
</view>
<view class="tab-bar-text" style="color: {{selected === 1 ? selectedColor : color}}">{{list[1].text}}</view>
</view>
<!-- 找伙伴 - 中间突出按钮(根据配置显示) -->
<view class="tab-bar-item special-item" wx:if="{{matchEnabled}}" data-path="{{list[2].pagePath}}" data-index="2" bindtap="switchTab">
<view class="special-button {{selected === 2 ? 'special-active' : ''}}">
<image class="special-icon"
src="/assets/icons/partners.svg"
mode="aspectFit"></image>
<view
wx:else
class="tab-bar-item"
data-path="{{item.pagePath}}"
data-index="{{index}}"
bindtap="switchTab"
>
<view class="icon-wrapper">
<image
class="tab-icon {{selected === index ? 'icon-active' : ''}}"
src="{{item.iconPath}}"
mode="aspectFit"
></image>
</view>
<view class="tab-bar-text" style="color: {{selected === index ? selectedColor : color}}">{{item.text}}</view>
</view>
<view class="tab-bar-text special-text" style="color: {{selected === 2 ? selectedColor : color}}">{{list[2].text}}</view>
</view>
<!-- 我的 -->
<view class="tab-bar-item" data-path="{{list[3].pagePath}}" data-index="{{matchEnabled ? 3 : 2}}" bindtap="switchTab">
<view class="icon-wrapper">
<image class="tab-icon {{(matchEnabled && selected === 3) || (!matchEnabled && selected === 2) ? 'icon-active' : ''}}"
src="/assets/icons/user.svg"
mode="aspectFit"
style="color: {{(matchEnabled && selected === 3) || (!matchEnabled && selected === 2) ? selectedColor : color}}"></image>
</view>
<view class="tab-bar-text" style="color: {{(matchEnabled && selected === 3) || (!matchEnabled && selected === 2) ? selectedColor : color}}">{{list[3].text}}</view>
</view>
</block>
</view>

View File

@@ -91,7 +91,7 @@ Page({
// 我的余额
walletBalanceText: '--',
// 超级个体:快捷入口「客资 / 链接轨迹」
// 超级个体:快捷入口「链接轨迹」
showSuperCrmEntry: false,
// 超级个体:发文章入口
showSuperArticleEntry: false,

View File

@@ -100,35 +100,30 @@
<view class="stat-icon-wrap">
<image class="stat-icon-img" src="/assets/icons/list-teal.svg" mode="aspectFit"/>
</view>
<text class="stat-num">订单</text>
<text class="stat-label">我的订单</text>
</view>
<view class="stat-box" hover-class="stat-box-hover" wx:if="{{!auditMode}}" bindtap="handleMenuTap" data-id="giftPay">
<view class="stat-icon-wrap">
<image class="stat-icon-img" src="/assets/icons/share-teal.svg" mode="aspectFit"/>
</view>
<text class="stat-num">代付</text>
<text class="stat-label">我的代付</text>
</view>
<view class="stat-box" hover-class="stat-box-hover" wx:if="{{!auditMode}}" bindtap="handleMenuTap" data-id="wallet">
<view class="stat-icon-wrap">
<image class="stat-icon-img" src="/assets/icons/wallet-teal.svg" mode="aspectFit"/>
</view>
<text class="stat-num stat-num-balance">{{walletBalanceText}}</text>
<text class="stat-label">我的余额</text>
</view>
<view class="stat-box" hover-class="stat-box-hover" wx:if="{{showSuperCrmEntry}}" bindtap="handleMenuTap" data-id="superCrm">
<view class="stat-icon-wrap">
<image class="stat-icon-img" src="/assets/icons/users-teal.svg" mode="aspectFit"/>
</view>
<text class="stat-num">客资</text>
<text class="stat-label">链接与轨迹</text>
<text class="stat-label">链接轨迹</text>
</view>
<view class="stat-box" hover-class="stat-box-hover" wx:if="{{showSuperArticleEntry && !auditMode}}" bindtap="handleMenuTap" data-id="superArticle">
<view class="stat-icon-wrap">
<image class="stat-icon-img" src="/assets/icons/book-arrow-teal.svg" mode="aspectFit"/>
</view>
<text class="stat-num">发文</text>
<text class="stat-label">发布文章</text>
</view>
</view>

View File

@@ -187,6 +187,11 @@
.card-icon-img { width: 40rpx; height: 40rpx; flex-shrink: 0; }
.card-title { font-size: 32rpx; font-weight: bold; color: #fff; }
/* 快捷入口:略减卡片底边距,避免第二行下方大块留白 */
.stats-card {
padding-bottom: 24rpx;
}
/* ===== 一键收款卡片 ===== */
.receive-card { padding: 28rpx 32rpx; }
.receive-top { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; }
@@ -219,15 +224,15 @@
/* 快捷入口:列数由页面 quickEntryGridCols 绑定≤4 铺满一行;>4 时为 4 列换行) */
.stats-grid {
display: grid;
gap: 24rpx;
gap: 20rpx 16rpx;
align-items: stretch;
}
.stat-box {
background: linear-gradient(155deg, rgba(42, 42, 42, 0.98) 0%, rgba(28, 35, 34, 0.99) 100%);
border-radius: 22rpx;
padding: 28rpx 16rpx 24rpx;
padding: 18rpx 12rpx 16rpx;
display: flex; flex-direction: column; align-items: center; justify-content: flex-start;
min-height: 168rpx;
min-height: 128rpx;
border: 1rpx solid rgba(79, 209, 197, 0.22);
box-shadow:
0 6rpx 22rpx rgba(0, 0, 0, 0.35),
@@ -247,7 +252,7 @@
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-bottom: 14rpx;
margin-bottom: 8rpx;
background: linear-gradient(145deg, rgba(79, 209, 197, 0.28) 0%, rgba(79, 209, 197, 0.06) 55%, rgba(20, 60, 55, 0.35) 100%);
border: 1rpx solid rgba(79, 209, 197, 0.38);
box-shadow:
@@ -256,22 +261,7 @@
inset 0 1rpx 0 rgba(255, 255, 255, 0.12);
}
.stat-icon-img { width: 48rpx; height: 48rpx; flex-shrink: 0; display: block; }
.stat-num {
font-size: 32rpx;
font-weight: 700;
color: #fff;
line-height: 1.25;
letter-spacing: 0.02em;
}
.stat-num-balance {
font-size: 30rpx;
font-variant-numeric: tabular-nums;
background: linear-gradient(90deg, #5eead4 0%, #4fd1c5 50%, #2dd4bf 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.stat-label { font-size: 20rpx; color: #8B939E; margin-top: 6rpx; line-height: 1.25; text-align: center; }
.stat-label { font-size: 22rpx; color: #8B939E; margin-top: 4rpx; line-height: 1.25; text-align: center; }
/* 最近阅读 */
.recent-list { display: flex; flex-direction: column; gap: 24rpx; }

View File

@@ -80,12 +80,15 @@ Page({
this.setData({ loading: false })
return
}
const imgs = Array.isArray(article.images)
? article.images.map((u) => String(u || '').trim()).filter(Boolean)
: []
this.setData({
article: {
...article,
article: Object.assign({}, article, {
createdAtText: formatTime(article.createdAt),
imageUrls: imgs,
contentHtml: renderArticleHtml(article.content),
},
}),
articleId: String(id),
h5ReadUrl: buildH5ReadUrl(id),
loading: false,
@@ -97,6 +100,15 @@ Page({
goBack() {
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/my/my' }) })
},
previewArticleImage(e) {
const current = e.currentTarget.dataset.current || ''
const urls = (this.data.article && this.data.article.imageUrls) || []
if (!urls.length) return
wx.previewImage({
urls,
current: current || urls[0],
})
},
copyH5Link() {
const url = this.data.h5ReadUrl
if (!url) {

View File

@@ -8,9 +8,20 @@
</view>
<view style="height: {{statusBarHeight + 44}}px;"></view>
<view class="card" wx:if="{{article}}">
<view class="article-body" wx:if="{{article}}">
<text class="title">{{article.title}}</text>
<text class="meta">{{article.authorNickname || '超级个体'}} · {{article.createdAtText}}</text>
<view class="img-gallery" wx:if="{{article.imageUrls.length}}">
<image
wx:for="{{article.imageUrls}}"
wx:key="*this"
class="article-gallery-img"
mode="widthFix"
src="{{item}}"
bindtap="previewArticleImage"
data-current="{{item}}"
/>
</view>
<view class="h5-row" wx:if="{{h5ReadUrl}}">
<view class="h5-btn" bindtap="copyH5Link">复制 H5 全文链接</view>
<text class="h5-hint">浏览器或微信内打开可阅读全文,方便分享</text>

View File

@@ -6,16 +6,30 @@
}
.nav-back, .nav-placeholder { width: 64rpx; }
.nav-title { font-size: 32rpx; font-weight: 700; }
.card {
margin: 24rpx; padding: 30rpx; border-radius: 24rpx;
background: rgba(15, 23, 42, 0.86); border: 1rpx solid rgba(148, 163, 184, 0.2);
.article-body {
padding: 8rpx 24rpx 48rpx;
}
.title { font-size: 36rpx; font-weight: 700; line-height: 1.4; display: block; }
.meta { margin-top: 12rpx; font-size: 22rpx; color: #94a3b8; display: block; }
.h5-row { margin-top: 20rpx; padding-top: 20rpx; border-top: 1rpx solid rgba(148, 163, 184, 0.2); }
.img-gallery {
margin-top: 20rpx;
display: flex;
flex-direction: column;
gap: 12rpx;
}
.article-gallery-img {
width: 100%;
display: block;
border-radius: 0;
}
.h5-row { margin-top: 28rpx; padding-top: 0; border-top: none; }
.h5-btn {
display: inline-block; padding: 12rpx 24rpx; border-radius: 999rpx; font-size: 24rpx; font-weight: 600;
color: #0ea5e9; background: rgba(14, 165, 233, 0.12); border: 1rpx solid rgba(14, 165, 233, 0.45);
display: inline-block;
padding: 12rpx 0;
font-size: 24rpx;
font-weight: 600;
color: #22d3ee;
background: transparent;
}
.h5-hint { display: block; margin-top: 12rpx; font-size: 20rpx; color: #64748b; line-height: 1.5; }
.content {

View File

@@ -15,10 +15,13 @@ function newAttachId() {
Page({
data: {
/** 临时隐藏「AI 写文章」卡片;需上线时再设为 true */
showAiWriteSection: false,
statusBarHeight: 44,
auditMode: false,
title: '',
content: '',
bodyImages: [],
saving: false,
meNickname: '',
aiPrompt: '',
@@ -99,6 +102,117 @@ Page({
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()
@@ -334,19 +448,30 @@ Page({
wx.showToast({ title: '请填写标题和正文', icon: 'none' })
return
}
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)
this.setData({ saving: true })
wx.showLoading({ title: '发布中…', mask: true })
try {
const res = await app.request({
url: '/api/miniprogram/super/articles',
method: 'POST',
data: { userId, title, content },
data: { userId, title, content, images },
})
if (!res?.success) throw new Error(res?.error || '发布失败')
this.setData({ title: '', content: '', bodyImages: [] })
wx.showToast({ title: '发布成功', icon: 'success' })
setTimeout(() => wx.navigateBack(), 400)
} catch (e) {
wx.showToast({ title: e.message || '发布失败', icon: 'none' })
} finally {
try {
wx.hideLoading()
} catch (_) {}
this.setData({ saving: false })
}
},

View File

@@ -8,8 +8,8 @@
</view>
<view style="height: {{statusBarHeight + 44}}px;"></view>
<!-- AI参考图片 + 提示词 -->
<view class="card">
<!-- AI参考图片 + 提示词开关showAiWriteSection -->
<view wx:if="{{showAiWriteSection}}" class="card">
<text class="section-title">AI 写文章</text>
<text class="label">参考图片(可多选)</text>
<view class="file-actions">
@@ -42,6 +42,19 @@
<input class="input" maxlength="40" placeholder="写个标题最多40字" value="{{title}}" bindinput="onTitleInput" disabled="{{auditMode}}" />
</view>
<text class="label">配图(可选,最多 9 张)</text>
<view class="file-actions">
<view class="add-file-btn" bindtap="chooseArticleImages"> 添加配图</view>
</view>
<view class="body-img-grid" wx:if="{{bodyImages.length}}">
<view wx:for="{{bodyImages}}" wx:key="id" class="body-img-cell">
<image wx:if="{{item.url}}" class="body-img-thumb" mode="aspectFill" src="{{item.url}}" />
<view wx:if="{{item.uploading}}" class="body-img-mask">上传中…</view>
<text class="body-img-remove" data-id="{{item.id}}" bindtap="removeArticleImage">×</text>
</view>
</view>
<text class="hint">配图会展示在正文上方(与书籍正文分离存储);单张建议不超 30MB。</text>
<text class="label">正文</text>
<view class="toolbar">
<view class="tool-btn" bindtap="insertMentionSelf">@自己</view>
@@ -52,7 +65,7 @@
</view>
<text class="hint">提示:可直接输入 @昵称 或 #链接(https://xxx)</text>
<view class="submit-btn" bindtap="submitArticle">{{saving ? '发布中…' : '发布文章'}}</view>
<view class="submit-btn {{saving ? 'submit-btn-loading' : ''}}" bindtap="submitArticle">{{saving ? '发布中…' : '发布文章'}}</view>
</view>
<view wx:if="{{auditMode}}" class="audit-mode-mask" catchtouchmove="preventMove">

View File

@@ -36,6 +36,54 @@
.file-remove {
flex-shrink: 0; font-size: 24rpx; color: #f97316; padding: 8rpx 12rpx;
}
.body-img-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-bottom: 12rpx;
}
.body-img-cell {
position: relative;
width: 160rpx;
height: 160rpx;
border-radius: 14rpx;
overflow: hidden;
background: rgba(2, 6, 23, 0.65);
border: 1rpx solid rgba(148, 163, 184, 0.18);
}
.body-img-thumb {
width: 100%;
height: 100%;
display: block;
}
.body-img-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);
}
.body-img-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;
}
.prompt-textarea { min-height: 200rpx; }
.generate-btn {
margin-top: 20rpx; height: 88rpx; border-radius: 16rpx;
@@ -60,6 +108,7 @@
color: #032b35; font-size: 30rpx; font-weight: 700;
display: flex; align-items: center; justify-content: center;
}
.submit-btn-loading { opacity: 0.72; pointer-events: none; }
/* 审核模式:盖住整页(含自定义导航),高于 .nav-bar 的 z-index: 10 */
.audit-mode-mask {

View File

@@ -0,0 +1,157 @@
/**
* 动态广场:超级个体图文列表(触底分页)
*/
const app = getApp()
const { isSafeImageSrc } = require('../../utils/imageUrl.js')
const PAGE_SIZE = 10
function avatarLetter(name) {
const s = String(name || '').trim()
if (!s) return '会'
const ch = s[0]
return /[\u4e00-\u9fff]/.test(ch) ? ch : '会'
}
function relativeTime(iso) {
if (!iso) return ''
const d = new Date(iso)
const t = d.getTime()
if (Number.isNaN(t)) return ''
const sec = Math.max(0, Math.floor((Date.now() - t) / 1000))
if (sec < 60) return '刚刚'
if (sec < 3600) return `${Math.floor(sec / 60)}分钟前`
if (sec < 86400) return `${Math.floor(sec / 3600)}小时前`
if (sec < 86400 * 7) return `${Math.floor(sec / 86400)}天前`
const m = `${d.getMonth() + 1}`.padStart(2, '0')
const day = `${d.getDate()}`.padStart(2, '0')
return `${m}-${day}`
}
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))
return Object.assign({}, r, {
authorNickname: nick,
avatarSrc: av,
avatarOk,
avatarLetter: avatarLetter(nick),
timeText: relativeTime(r.createdAt),
preview: String(r.preview || '').trim(),
displayImages,
})
})
}
Page({
data: {
statusBarHeight: 44,
feedList: [],
page: 1,
total: 0,
hasMore: true,
loading: true,
loadingMore: false,
},
async onLoad() {
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
try {
await app.getAuditMode()
} catch (_) {}
if (app.globalData.auditMode) {
wx.switchTab({ url: '/pages/index/index' })
return
}
this.reloadFeed()
},
async onShow() {
try {
await app.getAuditMode()
} catch (_) {}
if (app.globalData.auditMode) {
wx.switchTab({ url: '/pages/index/index' })
return
}
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
const tabBar = this.getTabBar()
if (tabBar && tabBar.loadFeatureConfig) tabBar.loadFeatureConfig()
if (tabBar && tabBar.updateSelected) tabBar.updateSelected()
}
},
onReachBottom() {
this.loadMore()
},
async reloadFeed() {
this.setData({
loading: true,
page: 1,
feedList: [],
hasMore: true,
total: 0,
})
try {
await this.fetchPage(1, true)
} finally {
this.setData({ loading: false })
}
},
async loadMore() {
if (this.data.loading || this.data.loadingMore || !this.data.hasMore) return
const next = this.data.page + 1
this.setData({ loadingMore: true })
try {
await this.fetchPage(next, false)
} finally {
this.setData({ loadingMore: false })
}
},
async fetchPage(page, replace) {
const res = await app.request({
url: `/api/miniprogram/super/articles/feed?page=${encodeURIComponent(String(page))}&pageSize=${PAGE_SIZE}`,
silent: true,
})
if (!res?.success) {
wx.showToast({ title: String(res?.error || '加载失败'), icon: 'none' })
if (replace) this.setData({ feedList: [], hasMore: false, total: 0 })
return
}
const list = mapFeedRows(res.list || [])
const total = Number(res.total) || 0
const merged = replace ? list : this.data.feedList.concat(list)
const hasMore = merged.length < total
this.setData({
feedList: merged,
page,
total,
hasMore,
})
},
openArticle(e) {
const id = e.currentTarget.dataset.id
if (!id) return
wx.navigateTo({
url: `/pages/super-article-detail/super-article-detail?id=${encodeURIComponent(String(id))}`,
})
},
previewArticleImages(e) {
const fi = Number(e.currentTarget.dataset.feedIndex)
const ii = Number(e.currentTarget.dataset.imgIndex)
const row = this.data.feedList[fi]
const urls = row && row.displayImages ? row.displayImages : []
if (!urls.length) return
const current = urls[ii] || urls[0]
wx.previewImage({ urls, current })
},
})

View File

@@ -0,0 +1,9 @@
{
"usingComponents": {
"icon": "/components/icon/icon"
},
"enablePullDownRefresh": false,
"onReachBottomDistance": 160,
"backgroundTextStyle": "light",
"backgroundColor": "#0b1220"
}

View File

@@ -0,0 +1,61 @@
<view class="page">
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-placeholder"></view>
<text class="nav-title">动态广场</text>
<view class="nav-placeholder"></view>
</view>
<view class="nav-offset" style="height: {{statusBarHeight + 44}}px;"></view>
<view class="feed-wrap">
<view class="empty-block" wx:if="{{!loading && feedList.length === 0}}">
<text class="empty-text">暂无动态</text>
<text class="empty-sub">超级个体发文后将出现在这里</text>
</view>
<block wx:for="{{feedList}}" wx:key="id">
<view class="feed-item" bindtap="openArticle" data-id="{{item.id}}">
<view class="feed-head">
<image
wx:if="{{item.avatarOk}}"
class="feed-avatar"
mode="aspectFill"
src="{{item.avatarSrc}}"
/>
<view wx:else class="feed-avatar feed-avatar-ph">{{item.avatarLetter}}</view>
<view class="feed-head-mid">
<text class="feed-name">{{item.authorNickname || '超级个体'}}</text>
<text class="feed-time">{{item.timeText}}</text>
</view>
</view>
<text class="feed-title" wx:if="{{item.title}}">{{item.title}}</text>
<text class="feed-preview">{{item.preview}}</text>
<view class="feed-imgs" wx:if="{{item.displayImages.length}}">
<image
wx:for="{{item.displayImages}}"
wx:for-item="img"
wx:for-index="ii"
wx:key="*this"
class="feed-img {{item.displayImages.length === 1 ? 'feed-img-one' : ''}}"
mode="aspectFill"
src="{{img}}"
catchtap="previewArticleImages"
data-feed-index="{{index}}"
data-img-index="{{ii}}"
/>
</view>
<view class="feed-foot">
<text class="feed-more">查看全文</text>
<icon name="chevron-right" size="28" color="rgba(34,211,238,0.75)" customClass="feed-more-icon"></icon>
</view>
</view>
</block>
<view class="load-more" wx:if="{{feedList.length}}">
<text wx:if="{{loadingMore}}">加载中…</text>
<text wx:elif="{{!hasMore}}">已经到底啦</text>
</view>
</view>
</view>

View File

@@ -0,0 +1,164 @@
.page {
min-height: 100vh;
background: #0b1220;
color: #e2e8f0;
padding-bottom: calc(220rpx + env(safe-area-inset-bottom, 0px));
box-sizing: border-box;
}
.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.92);
border-bottom: 1rpx solid rgba(148, 163, 184, 0.08);
}
.nav-placeholder {
width: 64rpx;
}
.nav-title {
font-size: 34rpx;
font-weight: 700;
color: #f8fafc;
}
.feed-wrap {
padding: 8rpx 0 32rpx;
}
.empty-block {
padding: 120rpx 32rpx;
text-align: center;
}
.empty-text {
display: block;
font-size: 30rpx;
color: #94a3b8;
}
.empty-sub {
display: block;
margin-top: 16rpx;
font-size: 24rpx;
color: #64748b;
}
/* 扁平信息流:无卡片底、无描边、无阴影 */
.feed-item {
padding: 28rpx 24rpx 36rpx;
}
.feed-head {
display: flex;
align-items: center;
gap: 16rpx;
}
.feed-avatar {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
flex-shrink: 0;
background: #1e293b;
}
.feed-avatar-ph {
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: 700;
color: #67e8f9;
background: rgba(30, 41, 59, 0.65);
}
.feed-head-mid {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 6rpx;
}
.feed-name {
font-size: 28rpx;
font-weight: 600;
color: #f1f5f9;
}
.feed-time {
font-size: 22rpx;
color: #94a3b8;
}
.feed-title {
display: block;
margin-top: 18rpx;
font-size: 30rpx;
font-weight: 700;
color: #f8fafc;
line-height: 1.45;
}
.feed-preview {
display: block;
margin-top: 12rpx;
font-size: 26rpx;
color: #cbd5e1;
line-height: 1.55;
white-space: pre-wrap;
word-break: break-word;
}
.feed-imgs {
margin-top: 16rpx;
display: flex;
flex-wrap: wrap;
gap: 10rpx;
}
.feed-img {
width: calc((100% - 20rpx) / 3);
height: 200rpx;
border-radius: 0;
}
.feed-img-one {
width: 100%;
height: 320rpx;
}
.feed-foot {
margin-top: 12rpx;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 4rpx;
}
.feed-more {
font-size: 24rpx;
color: rgba(34, 211, 238, 0.85);
font-weight: 500;
}
.feed-more-icon {
opacity: 0.85;
}
.load-more {
text-align: center;
padding: 24rpx 0 48rpx;
font-size: 24rpx;
color: #64748b;
}