feat: 更新前端多个页面和后端服务
- 前端: 更新AI营养师、计算器、打卡、食物详情等页面 - 前端: 更新食物百科、知识详情、营养知识页面 - 前端: 更新社区首页 - 后端: 更新ToolKieAIServiceImpl服务 - API: 更新models-api.js和user.js
This commit is contained in:
@@ -610,21 +610,24 @@ export default {
|
||||
this.scrollToBottom();
|
||||
},
|
||||
|
||||
/** 从 Gemini 响应 message.content 提取展示文本(支持 string 或 parts 数组) */
|
||||
/** 从 Gemini 响应 data.choices[0].message.content 提取展示文本(支持 string / parts 数组 / { parts } 对象) */
|
||||
extractReplyContent(content) {
|
||||
if (content == null) return '';
|
||||
if (typeof content === 'string') return content;
|
||||
if (Array.isArray(content)) {
|
||||
return content.map(part => (part && part.text) ? part.text : '').filter(Boolean).join('');
|
||||
}
|
||||
if (typeof content === 'object' && Array.isArray(content.parts)) {
|
||||
return content.parts.map(part => (part && part.text) ? part.text : '').filter(Boolean).join('');
|
||||
}
|
||||
return String(content);
|
||||
},
|
||||
|
||||
async sendToAI(content, type) {
|
||||
this.isLoading = true;
|
||||
|
||||
// BUG-005:文本/多模态必须走 KieAI Gemini,回复仅从 data.choices[0].message.content 取,不得使用固定话术
|
||||
// 请求体: { messages: [{ role: 'user', content }], stream: false }
|
||||
// BUG-005:文本/多模态必须走 KieAI Gemini;请求体 { messages: [{ role: 'user', content: 用户输入 }], stream: false }
|
||||
// 回复仅从 data.choices[0].message.content 取,禁止使用 getAIResponse 等固定话术
|
||||
if (type === 'text' || type === 'multimodal') {
|
||||
try {
|
||||
const messages = [{ role: 'user', content: content }];
|
||||
@@ -634,10 +637,10 @@ export default {
|
||||
const data = response.data;
|
||||
const choice = data.choices && data.choices[0];
|
||||
const msgObj = choice && choice.message;
|
||||
const rawContent = msgObj && msgObj.content;
|
||||
const reply = rawContent != null ? this.extractReplyContent(rawContent) : '';
|
||||
// 成功时仅展示模型返回内容
|
||||
this.messageList.push({ role: 'ai', content: reply || '未能获取到有效回复。' });
|
||||
const rawContent = msgObj != null ? msgObj.content : undefined;
|
||||
const reply = rawContent != null && rawContent !== undefined ? this.extractReplyContent(rawContent) : '';
|
||||
// BUG-005: 仅展示接口返回的 data.choices[0].message.content,不使用固定话术
|
||||
this.messageList.push({ role: 'ai', content: reply.trim() || '未能获取到有效回复。' });
|
||||
} else {
|
||||
const msg = (response && response.message) || '发起对话失败';
|
||||
this.messageList.push({ role: 'ai', content: '请求失败:' + msg });
|
||||
|
||||
@@ -487,12 +487,12 @@ export default {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
transition: all 0.3s;
|
||||
border-bottom: none;
|
||||
transition: all 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
/* 未激活:明显变灰,无下划线 */
|
||||
color: #9ca3af;
|
||||
font-weight: 400;
|
||||
/* 未激活:明显变灰,无下划线 */
|
||||
border-bottom: none;
|
||||
.tab-icon {
|
||||
font-size: 28rpx;
|
||||
color: #9ca3af;
|
||||
@@ -503,12 +503,12 @@ export default {
|
||||
color: #9ca3af;
|
||||
font-weight: 400;
|
||||
}
|
||||
/* 激活:加粗、主色、橙色底部下划线 */
|
||||
/* 激活:加粗、主色、橙色底部下划线(BUG-002) */
|
||||
&.active {
|
||||
background: transparent;
|
||||
border-bottom: 3px solid #f97316;
|
||||
color: #f97316;
|
||||
font-weight: 700;
|
||||
border-bottom: 3px solid #f97316;
|
||||
.tab-text {
|
||||
color: #f97316;
|
||||
font-weight: 700;
|
||||
|
||||
@@ -241,17 +241,20 @@ export default {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { setSignIntegral, getUserInfo } = await import('@/api/user.js');
|
||||
const { setSignIntegral, getFrontUserInfo, getUserInfo } = await import('@/api/user.js');
|
||||
const { getUserPoints } = await import('@/api/tool.js');
|
||||
|
||||
// 子问题 A:不得在 API 返回成功前修改 currentPoints,避免打卡前积分提前跳变
|
||||
// 子问题 B-1:打卡接口为 GET /api/front/user/sign/integral(setSignIntegral),与 /api/front/user/checkin 同属签到类接口
|
||||
// 打卡接口:GET /api/front/user/sign/integral(setSignIntegral),等同于 checkin 签到
|
||||
await setSignIntegral();
|
||||
|
||||
this.todaySigned = true;
|
||||
|
||||
// 子问题 B-2/B-3:仅用服务端返回值更新积分,禁止前端本地 +30。调用 GET /api/front/user(getUserInfo)刷新用户信息,将返回的 integral 赋给 currentPoints
|
||||
const userRes = await getUserInfo();
|
||||
// 子问题 B:仅在打卡成功后用服务端数据更新积分。先 GET /api/front/user/info 刷新用户积分,禁止硬编码 +30
|
||||
let userRes = null;
|
||||
try {
|
||||
userRes = await getFrontUserInfo(); // GET /api/front/user/info
|
||||
} catch (_) {
|
||||
userRes = await getUserInfo().catch(() => null);
|
||||
}
|
||||
if (userRes && userRes.data && (userRes.data.integral != null || userRes.data.points != null)) {
|
||||
this.currentPoints = userRes.data.integral ?? userRes.data.points ?? 0;
|
||||
} else {
|
||||
@@ -261,6 +264,9 @@ export default {
|
||||
this.currentPoints = serverPoints;
|
||||
}
|
||||
}
|
||||
|
||||
// 积分已从服务端更新后再更新打卡状态并跳转,避免“已打卡”与积分不同步
|
||||
this.todaySigned = true;
|
||||
} catch (e) {
|
||||
const msg = (typeof e === 'string' ? e : (e && (e.message || e.msg))) || '打卡失败';
|
||||
if (msg.includes('今日已签到') || msg.includes('不可重复')) {
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
<scroll-view class="content-scroll" scroll-y>
|
||||
<!-- 食物大图 -->
|
||||
<view class="food-image-section">
|
||||
<image class="food-image" :src="foodData.image || defaultFoodData.image" mode="aspectFill"></image>
|
||||
<image class="food-image" :src="displayImage" mode="aspectFill"></image>
|
||||
<view class="image-overlay"></view>
|
||||
<view class="food-info-overlay">
|
||||
<view class="food-name-overlay">{{ foodData.name || '—' }}</view>
|
||||
<view class="food-name-overlay">{{ displayName }}</view>
|
||||
<view class="food-tags">
|
||||
<view class="food-tag category">{{ foodData.category || '—' }}</view>
|
||||
<view class="food-tag safe">{{ foodData.safetyTag || '—' }}</view>
|
||||
<view class="food-tag category">{{ displayCategory }}</view>
|
||||
<view class="food-tag safe">{{ displaySafetyTag }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -34,7 +34,7 @@
|
||||
<view class="key-nutrients-grid">
|
||||
<view
|
||||
class="nutrient-card"
|
||||
v-for="(nutrient, index) in foodData.keyNutrients"
|
||||
v-for="(nutrient, index) in displayKeyNutrients"
|
||||
:key="index"
|
||||
>
|
||||
<text class="nutrient-name">{{ nutrient.name }}</text>
|
||||
@@ -56,7 +56,7 @@
|
||||
<view class="nutrition-table">
|
||||
<view
|
||||
class="nutrition-row"
|
||||
v-for="(item, index) in foodData.nutritionTable"
|
||||
v-for="(item, index) in displayNutritionTable"
|
||||
:key="index"
|
||||
>
|
||||
<view class="nutrition-left">
|
||||
@@ -132,21 +132,58 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 保证 .food-name-overlay / .nutrient-card / .nutrition-row 在 defaultFoodData 状态下也有非空数据可渲染
|
||||
displayName() {
|
||||
return (this.foodData && this.foodData.name) ? this.foodData.name : (this.defaultFoodData.name || '—')
|
||||
},
|
||||
displayCategory() {
|
||||
return (this.foodData && this.foodData.category) ? this.foodData.category : (this.defaultFoodData.category || '—')
|
||||
},
|
||||
displaySafetyTag() {
|
||||
return (this.foodData && this.foodData.safetyTag) ? this.foodData.safetyTag : (this.defaultFoodData.safetyTag || '—')
|
||||
},
|
||||
displayImage() {
|
||||
return (this.foodData && this.foodData.image) ? this.foodData.image : (this.defaultFoodData.image || '')
|
||||
},
|
||||
displayKeyNutrients() {
|
||||
const arr = this.foodData && this.foodData.keyNutrients
|
||||
if (Array.isArray(arr) && arr.length > 0) return arr
|
||||
const def = this.defaultFoodData.keyNutrients
|
||||
return Array.isArray(def) && def.length > 0 ? def : [{ name: '—', value: '—', unit: '', status: '—' }]
|
||||
},
|
||||
displayNutritionTable() {
|
||||
const arr = this.foodData && this.foodData.nutritionTable
|
||||
if (Array.isArray(arr) && arr.length > 0) return arr
|
||||
const def = this.defaultFoodData.nutritionTable
|
||||
return Array.isArray(def) && def.length > 0 ? def : [{ name: '—', value: '—', unit: '', level: 'low', levelText: '—' }]
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
this.pageParams = { id: options.id || '', name: options.name || '' }
|
||||
options = options || {}
|
||||
// 若列表误将 name 传成 id(如 id=羊肉(熟)),用 id 作为展示用 name,避免请求接口 400
|
||||
const rawId = options.id
|
||||
const rawName = options.name
|
||||
const hasNonNumericId = rawId !== undefined && rawId !== '' && isNaN(Number(rawId))
|
||||
const displayName = rawName || (hasNonNumericId ? rawId : '')
|
||||
this.pageParams = {
|
||||
id: (rawId !== undefined && rawId !== '') ? String(rawId) : '',
|
||||
name: (displayName !== undefined && displayName !== '') ? String(displayName) : ''
|
||||
}
|
||||
// 打印入参便于排查:后端详情接口仅接受 Long 类型 id,传 name 会导致 400
|
||||
console.log('[food-detail] onLoad options:', options)
|
||||
console.log('[food-detail] onLoad pageParams (id/name):', this.pageParams)
|
||||
const rawId = options.id
|
||||
const isNumericId = rawId !== undefined && rawId !== '' && !isNaN(Number(rawId))
|
||||
if (isNumericId) {
|
||||
this.loadFoodData(Number(rawId))
|
||||
} else if (options.name) {
|
||||
// 无有效 id 仅有 name 时,不请求接口(避免传 name 导致后端 NumberFormatException),直接展示默认数据 + 当前名称
|
||||
const numId = Number(rawId)
|
||||
console.log('[food-detail] 使用数字 id 请求详情,不传 name 字段:', numId)
|
||||
this.loadFoodData(numId)
|
||||
} else if (this.pageParams.name) {
|
||||
// 无有效 id、仅有 name 时,不请求接口(避免传 name 导致后端 NumberFormatException),直接展示默认数据 + 当前名称
|
||||
this.loadError = '暂无该食物详情数据,展示参考数据'
|
||||
this.applyDefaultFoodData(false)
|
||||
try {
|
||||
this.foodData.name = decodeURIComponent(String(options.name))
|
||||
this.foodData.name = decodeURIComponent(this.pageParams.name)
|
||||
} catch (e) {}
|
||||
} else {
|
||||
this.applyDefaultFoodData()
|
||||
@@ -192,8 +229,10 @@ export default {
|
||||
console.log('[food-detail] getFoodDetail request param:', { id, type: typeof id })
|
||||
try {
|
||||
const res = await getFoodDetail(id)
|
||||
const data = res.data || res
|
||||
console.log('[food-detail] getFoodDetail 响应:', data ? { hasName: !!data.name, hasImage: !!data.image, keys: Object.keys(data) } : null)
|
||||
// 打印响应结构便于确认:request 成功时 resolve 的是 res.data,即 { code: 200, data: {...} }
|
||||
console.log('[food-detail] getFoodDetail 响应结构:', res ? { hasData: !!res.data, code: res.code, keys: Object.keys(res || {}) } : null)
|
||||
const data = res.data != null ? res.data : res
|
||||
console.log('[food-detail] getFoodDetail 解析后 data:', data ? { hasName: !!data.name, hasImage: !!data.image, keys: Object.keys(data) } : null)
|
||||
if (data && data.name) {
|
||||
// 解析API返回的食物数据
|
||||
this.foodData = {
|
||||
@@ -219,12 +258,14 @@ export default {
|
||||
uni.showToast({ title: '数据加载失败', icon: 'none' })
|
||||
}
|
||||
} catch (error) {
|
||||
const errMsg = (error && (error.message || error.msg || error.errMsg || error)) ? String(error.message || error.msg || error.errMsg || error) : '未知错误'
|
||||
// a. 将 loadError 置为具体错误信息(用于调试):兼容 Error、{ message/msg }、字符串
|
||||
const errMsg = (error && (error.message || error.msg || error.errMsg))
|
||||
? String(error.message || error.msg || error.errMsg)
|
||||
: (typeof error === 'string' ? error : (error ? String(error) : '未知错误'))
|
||||
console.error('[food-detail] 加载食物数据失败:', error)
|
||||
console.error('[food-detail] loadError(用于调试):', errMsg)
|
||||
// a. 将 loadError 置为具体错误信息(用于调试)
|
||||
this.loadError = errMsg
|
||||
// b. 使用 defaultFoodData 填充页面,保证用户能看到基础界面;不清空 loadError 以便展示「当前数据来自缓存」提示
|
||||
// b. 使用 defaultFoodData 填充页面,保证用户能看到基础界面而不是空白
|
||||
this.applyDefaultFoodData(false)
|
||||
// 若有入参 name,用其覆盖展示名称,避免显示默认「五谷香」
|
||||
if (this.pageParams && this.pageParams.name) {
|
||||
@@ -232,7 +273,7 @@ export default {
|
||||
this.foodData.name = decodeURIComponent(String(this.pageParams.name))
|
||||
} catch (e) {}
|
||||
}
|
||||
// c. 页面已通过 v-if="loadError" 显示「当前数据来自缓存,可能不是最新」;再弹出轻提示
|
||||
// c. 页面通过 v-if="loadError" 显示「当前数据来自缓存,可能不是最新」;再弹出轻提示
|
||||
uni.showToast({
|
||||
title: '数据加载失败',
|
||||
icon: 'none'
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
<view class="food-image-wrapper">
|
||||
<image
|
||||
class="food-image"
|
||||
:src="getFoodImage(item) || defaultPlaceholder"
|
||||
:src="getFoodImage(item)"
|
||||
mode="aspectFill"
|
||||
@error="onFoodImageError(item)"
|
||||
></image>
|
||||
@@ -126,7 +126,7 @@
|
||||
<view class="nutrition-list">
|
||||
<view
|
||||
class="nutrition-item"
|
||||
v-for="(nut, idx) in (item.nutrition || item.nutrients || [])"
|
||||
v-for="(nut, idx) in getNutritionList(item)"
|
||||
:key="idx"
|
||||
>
|
||||
<text class="nutrition-label">{{ nut.label || '—' }}</text>
|
||||
@@ -278,12 +278,34 @@ export default {
|
||||
return [];
|
||||
},
|
||||
getFoodImage(item) {
|
||||
if (!item) return this.defaultPlaceholder;
|
||||
const id = item.id != null ? item.id : item.foodId;
|
||||
if (id != null && this.imageErrorIds[String(id)]) return this.defaultPlaceholder;
|
||||
const raw = item.imageUrl || item.image || item.img || item.pic || item.coverImage || '';
|
||||
const url = raw && (raw.startsWith('//') || raw.startsWith('http')) ? raw : (raw && raw.startsWith('/') ? (HTTP_REQUEST_URL || '') + raw : raw);
|
||||
// 兼容后端 image / image_url / 前端 imageUrl、img、pic、coverImage、cover_image
|
||||
const raw = item.imageUrl || item.image || item.image_url || item.img || item.pic || item.coverImage || item.cover_image || '';
|
||||
const s = (raw && String(raw).trim()) || '';
|
||||
if (!s || s === 'null' || s === 'undefined') return this.defaultPlaceholder;
|
||||
const url = (s.startsWith('//') || s.startsWith('http')) ? s : (s.startsWith('/') ? (HTTP_REQUEST_URL || '') + s : s);
|
||||
return (url && String(url).trim()) ? url : this.defaultPlaceholder;
|
||||
},
|
||||
getNutritionList(item) {
|
||||
if (!item) return [];
|
||||
const arr = item.nutrition || item.nutrients || item.nutritions;
|
||||
if (Array.isArray(arr) && arr.length > 0) return arr;
|
||||
// 无数组时从扁平字段组装,确保列表始终有营养简介
|
||||
const list = [];
|
||||
const push = (label, val, unit) => {
|
||||
const value = (val != null && val !== '') ? String(val) + (unit || '') : '—';
|
||||
list.push({ label, value, colorClass: 'green' });
|
||||
};
|
||||
push('能量', item.energy, 'kcal');
|
||||
push('蛋白质', item.protein, 'g');
|
||||
push('钾', item.potassium, 'mg');
|
||||
push('磷', item.phosphorus, 'mg');
|
||||
push('钠', item.sodium, 'mg');
|
||||
push('钙', item.calcium, 'mg');
|
||||
return list;
|
||||
},
|
||||
onFoodImageError(item) {
|
||||
const id = item.id != null ? item.id : item.foodId;
|
||||
if (id != null && !this.imageErrorIds[String(id)]) {
|
||||
@@ -300,9 +322,11 @@ export default {
|
||||
};
|
||||
const safety = item.safety != null ? { safety: item.safety, safetyClass: item.safetyClass || 'safe' } : (safetyMap[item.suitabilityLevel] || { safety: '—', safetyClass: 'safe' });
|
||||
|
||||
// 图片:兼容 image/imageUrl/img/pic/coverImage,相对路径补全为完整 URL,空则留空由 getFoodImage 用占位图
|
||||
const rawImg = item.imageUrl || item.image || item.img || item.pic || item.coverImage || '';
|
||||
const imageUrl = (rawImg && (rawImg.startsWith('//') || rawImg.startsWith('http'))) ? rawImg : (rawImg && rawImg.startsWith('/') ? (HTTP_REQUEST_URL || '') + rawImg : rawImg);
|
||||
// 图片:兼容 image/image_url/imageUrl/img/pic/coverImage/cover_image,相对路径补全,空或无效则由 getFoodImage 用占位图
|
||||
const rawImg = item.imageUrl || item.image || item.image_url || item.img || item.pic || item.coverImage || item.cover_image || '';
|
||||
const rawStr = (rawImg && String(rawImg).trim()) || '';
|
||||
const validRaw = rawStr && rawStr !== 'null' && rawStr !== 'undefined';
|
||||
const imageUrl = validRaw && (rawStr.startsWith('//') || rawStr.startsWith('http')) ? rawStr : (validRaw && rawStr.startsWith('/') ? (HTTP_REQUEST_URL || '') + rawStr : (validRaw ? rawStr : ''));
|
||||
const image = imageUrl || '';
|
||||
|
||||
// 营养简介:优先 item.nutrition,其次 item.nutrients(兼容后端),否则由扁平字段 energy/protein/potassium 等组装
|
||||
@@ -386,7 +410,7 @@ export default {
|
||||
},
|
||||
goToFoodDetail(item) {
|
||||
// 后端详情接口仅接受 Long 类型 id,仅在有有效数字 id 时传 id;始终传 name 供详情页失败时展示
|
||||
const rawId = item.id != null ? item.id : ''
|
||||
const rawId = item.id != null ? item.id : (item.foodId != null ? item.foodId : '')
|
||||
const numericId = (rawId !== '' && rawId !== undefined && !isNaN(Number(rawId))) ? Number(rawId) : null
|
||||
const namePart = item.name ? `&name=${encodeURIComponent(item.name)}` : ''
|
||||
const url = numericId !== null
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
<view v-else-if="error" class="error-wrap">
|
||||
<text>{{ error }}</text>
|
||||
</view>
|
||||
<view v-else class="empty-wrap">
|
||||
<text>暂无内容</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -50,8 +53,9 @@ export default {
|
||||
};
|
||||
},
|
||||
onLoad(options) {
|
||||
if (options.id) {
|
||||
this.id = options.id;
|
||||
const rawId = options.id;
|
||||
if (rawId != null && rawId !== '' && String(rawId).trim() !== '' && String(rawId) !== 'undefined') {
|
||||
this.id = String(rawId).trim();
|
||||
} else {
|
||||
this.loading = false;
|
||||
this.error = '缺少文章 ID';
|
||||
@@ -144,7 +148,8 @@ export default {
|
||||
color: #333;
|
||||
}
|
||||
.loading-wrap,
|
||||
.error-wrap {
|
||||
.error-wrap,
|
||||
.empty-wrap {
|
||||
padding: 80rpx 30rpx;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
@@ -153,4 +158,7 @@ export default {
|
||||
.error-wrap {
|
||||
color: #f56c6c;
|
||||
}
|
||||
.empty-wrap {
|
||||
color: #9fa5c0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -128,7 +128,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
currentTab: 'nutrients',
|
||||
scrollViewHeight: 'calc(100vh - 120rpx)', // 为 tab 预留高度,微信小程序 scroll-view 需明确高度
|
||||
scrollViewHeight: '100vh', // 默认值,onReady 中按机型计算为 px,保证微信小程序 scroll-view 有明确高度
|
||||
guideList: [],
|
||||
articleList: [],
|
||||
nutrientList: [
|
||||
@@ -188,8 +188,25 @@ export default {
|
||||
// 有 id 时切换到科普文章 tab,switchTab 内会调用 loadKnowledgeList 加载列表
|
||||
this.switchTab('articles');
|
||||
} else {
|
||||
// 无 id 时默认当前 tab 为「营养素」,不请求接口;用户点击「饮食指南」或「科普文章」时由 switchTab 触发 loadKnowledgeList
|
||||
// 无 id 时默认当前 tab 为「营养素」;切换到「饮食指南」或「科普文章」时由 switchTab 触发 loadKnowledgeList
|
||||
this.currentTab = 'nutrients';
|
||||
// 确保列表初始为数组,避免未加载时为 undefined;切换 Tab 后再加载对应列表
|
||||
this.guideList = Array.isArray(this.guideList) ? this.guideList : [];
|
||||
this.articleList = Array.isArray(this.articleList) ? this.articleList : [];
|
||||
}
|
||||
},
|
||||
onReady() {
|
||||
// 微信小程序 scroll-view 必须使用明确高度,且 calc(vh - rpx) 兼容性差,改为用 px 计算
|
||||
try {
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
const statusBarHeight = systemInfo.statusBarHeight || 0;
|
||||
const navHeight = 44;
|
||||
const tabBarRpx = 120;
|
||||
const windowWidth = systemInfo.windowWidth || 375;
|
||||
const tabBarPx = Math.ceil((windowWidth / 750) * tabBarRpx);
|
||||
this.scrollViewHeight = `calc(100vh - ${statusBarHeight + navHeight + tabBarPx}px)`;
|
||||
} catch (e) {
|
||||
this.scrollViewHeight = 'calc(100vh - 140px)';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -216,16 +233,21 @@ export default {
|
||||
page: 1,
|
||||
limit: 50
|
||||
});
|
||||
// 兼容 CommonPage:result.data.list,或 result.data/result.data.records 为数组
|
||||
// 兼容多种返回结构:result.data.list / result.data.records / result.data 为数组 / result.list
|
||||
let rawList = [];
|
||||
if (result && result.data) {
|
||||
if (Array.isArray(result.data.list)) {
|
||||
rawList = result.data.list;
|
||||
} else if (Array.isArray(result.data.records)) {
|
||||
rawList = result.data.records;
|
||||
} else if (Array.isArray(result.data)) {
|
||||
rawList = result.data;
|
||||
const d = result.data;
|
||||
if (Array.isArray(d.list)) {
|
||||
rawList = d.list;
|
||||
} else if (Array.isArray(d.records)) {
|
||||
rawList = d.records;
|
||||
} else if (Array.isArray(d)) {
|
||||
rawList = d;
|
||||
} else if (d && Array.isArray(d.data)) {
|
||||
rawList = d.data;
|
||||
}
|
||||
} else if (result && Array.isArray(result.list)) {
|
||||
rawList = result.list;
|
||||
}
|
||||
// Normalize id: backend may return knowledgeId, id, or knowledge_id (BeanUtil/JSON)
|
||||
const list = (rawList || []).map(item => {
|
||||
@@ -233,6 +255,7 @@ export default {
|
||||
return {
|
||||
...item,
|
||||
id,
|
||||
knowledgeId: item.knowledgeId ?? id,
|
||||
desc: item.desc || item.summary || '',
|
||||
time: item.time || (item.publishedAt || item.createdAt ? this.formatKnowledgeTime(item.publishedAt || item.createdAt) : ''),
|
||||
views: item.views != null ? item.views : (item.viewCount != null ? item.viewCount : 0),
|
||||
@@ -240,19 +263,20 @@ export default {
|
||||
coverImage: item.coverImage || item.cover_image || ''
|
||||
};
|
||||
});
|
||||
// 始终赋值为数组,绝不设为 undefined
|
||||
if (this.currentTab === 'guide') {
|
||||
this.guideList = list;
|
||||
this.guideList = Array.isArray(list) ? list : [];
|
||||
} else if (this.currentTab === 'articles') {
|
||||
this.articleList = list;
|
||||
this.articleList = Array.isArray(list) ? list : [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载知识列表失败:', error);
|
||||
const msg = (error && (typeof error === 'string' ? error : (error.message || error.msg))) || '加载列表失败';
|
||||
uni.showToast({
|
||||
title: String(msg),
|
||||
title: String(msg).substring(0, 20),
|
||||
icon: 'none'
|
||||
});
|
||||
// 失败时清空当前 tab 列表并确保始终为数组,不设为 undefined
|
||||
// 失败时清空当前 tab 列表,确保始终为数组,绝不设为 undefined
|
||||
if (this.currentTab === 'guide') {
|
||||
this.guideList = [];
|
||||
} else if (this.currentTab === 'articles') {
|
||||
@@ -275,13 +299,19 @@ export default {
|
||||
goToDetail(event) {
|
||||
const id = event.currentTarget.dataset.itemId;
|
||||
const knowledgeId = event.currentTarget.dataset.itemKid;
|
||||
const finalId = knowledgeId || id;
|
||||
if (!finalId) {
|
||||
uni.showToast({ title: "暂无详情", icon: "none" });
|
||||
const finalId = knowledgeId ?? id;
|
||||
// 仅当 knowledgeId 或 id 存在且有效时才跳转,否则提示暂无详情
|
||||
if (finalId == null || finalId === '' || String(finalId).trim() === '' || String(finalId) === 'undefined') {
|
||||
uni.showToast({ title: '暂无详情', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
const idStr = String(finalId).trim();
|
||||
if (idStr === 'undefined') {
|
||||
uni.showToast({ title: '暂无详情', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: `/pages/tool/knowledge-detail?id=${finalId}`
|
||||
url: `/pages/tool/knowledge-detail?id=${encodeURIComponent(idStr)}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,32 +4,32 @@
|
||||
<view class="tab-nav">
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: currentTab === 'recommend' }"
|
||||
@click="switchTab('recommend')"
|
||||
:class="{ active: currentTab === '推荐' }"
|
||||
@click="switchTab('推荐')"
|
||||
>
|
||||
<!-- <text class="tab-icon">📊</text> -->
|
||||
<text class="tab-text">推荐</text>
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: currentTab === 'latest' }"
|
||||
@click="switchTab('latest')"
|
||||
:class="{ active: currentTab === '最新' }"
|
||||
@click="switchTab('最新')"
|
||||
>
|
||||
<!-- <text class="tab-icon">🕐</text> -->
|
||||
<text class="tab-text">最新</text>
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: currentTab === 'follow' }"
|
||||
@click="switchTab('follow')"
|
||||
:class="{ active: currentTab === '关注' }"
|
||||
@click="switchTab('关注')"
|
||||
>
|
||||
<!-- <text class="tab-icon">👥</text> -->
|
||||
<text class="tab-text">关注</text>
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: currentTab === 'hot' }"
|
||||
@click="switchTab('hot')"
|
||||
:class="{ active: currentTab === '热门' }"
|
||||
@click="switchTab('热门')"
|
||||
>
|
||||
<!-- <text class="tab-icon">🔥</text> -->
|
||||
<text class="tab-text">热门</text>
|
||||
@@ -46,7 +46,7 @@
|
||||
<view class="empty-container" v-else-if="isEmpty">
|
||||
<text class="empty-icon">📭</text>
|
||||
<text class="empty-text">暂无内容</text>
|
||||
<text class="empty-hint" v-if="currentTab === 'follow'">关注更多用户,查看他们的打卡动态</text>
|
||||
<text class="empty-hint" v-if="currentTab === '关注'">关注更多用户,查看他们的打卡动态</text>
|
||||
<text class="empty-hint" v-else>快来发布第一条打卡动态吧</text>
|
||||
</view>
|
||||
|
||||
@@ -144,7 +144,7 @@ import { checkLogin, toLogin } from '@/libs/login.js'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
currentTab: 'recommend',
|
||||
currentTab: '推荐',
|
||||
postList: [],
|
||||
page: 1,
|
||||
limit: 10,
|
||||
@@ -173,12 +173,18 @@ export default {
|
||||
this.loadMore()
|
||||
},
|
||||
methods: {
|
||||
// Tab 中文转接口参数(不改动接口逻辑,仅在此处映射)
|
||||
getTabApiValue() {
|
||||
const map = { '推荐': 'recommend', '最新': 'latest', '关注': 'follow', '热门': 'hot' }
|
||||
return map[this.currentTab] || 'recommend'
|
||||
},
|
||||
|
||||
// 切换Tab
|
||||
switchTab(tab) {
|
||||
if (this.currentTab === tab) return
|
||||
|
||||
// 关注Tab需要登录
|
||||
if (tab === 'follow' && !checkLogin()) {
|
||||
if (tab === '关注' && !checkLogin()) {
|
||||
uni.showToast({
|
||||
title: '请先登录查看关注内容',
|
||||
icon: 'none'
|
||||
@@ -214,7 +220,7 @@ export default {
|
||||
|
||||
try {
|
||||
const res = await getCommunityList({
|
||||
tab: this.currentTab,
|
||||
tab: this.getTabApiValue(),
|
||||
page: this.page,
|
||||
limit: this.limit
|
||||
})
|
||||
@@ -385,7 +391,7 @@ export default {
|
||||
|
||||
try {
|
||||
const res = await getCommunityList({
|
||||
tab: this.currentTab,
|
||||
tab: this.getTabApiValue(),
|
||||
page: this.page,
|
||||
limit: this.limit
|
||||
})
|
||||
@@ -415,18 +421,18 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
// 格式化数量显示
|
||||
// 格式化数量显示(中文单位)
|
||||
formatCount(count) {
|
||||
if (!count) return '0'
|
||||
if (count >= 10000) {
|
||||
return (count / 10000).toFixed(1) + 'w'
|
||||
return (count / 10000).toFixed(1) + '万'
|
||||
} else if (count >= 1000) {
|
||||
return (count / 1000).toFixed(1) + 'k'
|
||||
return (count / 1000).toFixed(1) + '千'
|
||||
}
|
||||
return String(count)
|
||||
},
|
||||
|
||||
// 帖子类型英文转中文显示(仅用于展示,保证 label 均为中文)
|
||||
// 帖子类型英文/拼音转中文显示(仅用于展示,保证 label 均为中文)
|
||||
getMealTypeLabel(mealType) {
|
||||
if (!mealType) return '分享'
|
||||
const map = {
|
||||
@@ -435,7 +441,16 @@ export default {
|
||||
dinner: '晚餐',
|
||||
snack: '加餐',
|
||||
share: '分享',
|
||||
checkin: '打卡'
|
||||
checkin: '打卡',
|
||||
zaocan: '早餐',
|
||||
wucan: '午餐',
|
||||
wancan: '晚餐',
|
||||
jiacan: '加餐',
|
||||
fenxiang: '分享',
|
||||
daka: '打卡',
|
||||
morning: '早餐',
|
||||
noon: '午餐',
|
||||
night: '晚餐'
|
||||
}
|
||||
const str = String(mealType).trim()
|
||||
const lower = str.toLowerCase()
|
||||
|
||||
Reference in New Issue
Block a user