feat: 更新前端多个页面和后端服务

- 前端: 更新AI营养师、计算器、打卡、食物详情等页面
- 前端: 更新食物百科、知识详情、营养知识页面
- 前端: 更新社区首页
- 后端: 更新ToolKieAIServiceImpl服务
- API: 更新models-api.js和user.js
This commit is contained in:
2026-03-07 22:26:37 +08:00
parent 1632801880
commit f692c75f7b
11 changed files with 221 additions and 85 deletions

View File

@@ -390,6 +390,7 @@ public class ToolKieAIServiceImpl implements ToolKieAIService {
return emitter;
}
/** BUG-005: 仅使用 request.getMessages() 透传用户/助手消息,不注入硬编码 prompt */
private Map<String, Object> buildGeminiRequestBody(KieAIGeminiChatRequest request, boolean stream) {
Map<String, Object> body = new HashMap<>();
List<Map<String, Object>> messagesOut = new ArrayList<>();

View File

@@ -332,11 +332,12 @@ function queryAsrStatus(taskId) {
* @returns {Promise} 对话响应
*/
/**
* KieAI Gemini 2.5 Flash 对话(非流式
* KieAI Gemini 对话POST /api/front/kieai/gemini/chat
* 文本对话请求体: { messages: [{ role: 'user', content: 用户输入 }], stream: false }
* @param {object} data 请求体
* @param {Array} data.messages 消息列表 [{ role: 'user'|'assistant'|'system', content: string }]
* @param {Array} data.messages 消息列表 [{ role: 'user'|'assistant'|'system', content: string|Array }]
* @param {boolean} data.stream 是否流式,默认 false
* @returns {Promise} 响应 data 为 Gemini API 格式 { choices: [{ message: { content } }] }
* @returns {Promise} 响应 data 为 Gemini 格式 { choices: [{ message: { content } }] },回复取 data.choices[0].message.content
*/
function kieaiGeminiChat(data) {
return request('/api/front/kieai/gemini/chat', {

View File

@@ -19,6 +19,13 @@ export function getUserInfo(){
return request.get('user');
}
/**
* 获取用户信息GET /api/front/user/info用于打卡后刷新积分等
*/
export function getFrontUserInfo(){
return request.get('user/info');
}
/**
* 设置用户分享
*

View File

@@ -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 });

View File

@@ -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;

View File

@@ -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/integralsetSignIntegral与 /api/front/user/checkin 同属签到类接口
// 打卡接口GET /api/front/user/sign/integralsetSignIntegral等同于 checkin 签到
await setSignIntegral();
this.todaySigned = true;
// 子问题 B-2/B-3仅用服务端返回值更新积分禁止前端本地 +30。调用 GET /api/front/usergetUserInfo刷新用户信息将返回的 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('不可重复')) {

View File

@@ -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'

View File

@@ -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

View File

@@ -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>

View File

@@ -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 时切换到科普文章 tabswitchTab 内会调用 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
});
// 兼容 CommonPageresult.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)}`
});
}
}

View File

@@ -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()