Files

595 lines
23 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import envConfig from "../../env";
import { Comment, CommentResponse } from "./article";
interface IApp {
globalData: {
token: string;
userInfo: any;
rawCardData: any[];
processedCardsData: any[]; // 确保这里有定义
};
processedCardsData: any[]; // 也要在这里定义,因为您直接将其挂载在 App 实例上
}
const app = getApp<IApp>();
// 假设 Comment 和 CommentResponse 已经正确定义
// 比如在 article.ts 文件中
// export interface Comment { /* ... */ }
// export interface CommentResponse { success: boolean; message?: string; data: Comment[] | any; }
Component({
data: {
// 明确声明类型,解决 TypeScript 的 never[] 报错
comments: [] as Comment[],
loading: true,
error: null as string | null,
totalCommentsCount: 0, // *** 新增:存储所有评论的总数 (包括回复) ***
cardReviewData: null as any | null,
showReplyInput: false, // 控制输入框是否显示
currentReplyId: '', // 当前正在回复的评论ID
currentReplyUsername: '', // 当前正在回复的用户名(用于 placeholder
replyContent: '', // 输入框中的内容
isSubmitDisabled: true,
},
properties: {
id: {
type: String,
value: ''
}
},
lifetimes: {
attached() {
// 组件被挂载时执行
const id = this.properties.id;
this.getCardDetail(id);
this.loadCardReviewData(id);
}
},
methods: {
closeInput() {
if (this.data.showReplyInput) {
this.setData({
showReplyInput: false,
currentReplyId: null,
currentReplyUsername: '',
replyContent: '',
isSubmitDisabled: true,
keyboardHeight: 0
});
}
},
/**
* 页面滚动时,调用关闭函数
*/
onPageScroll() {
this.closeInput();
},
/**
* 点击“空白”区域(现在是整个 scroll-view调用关闭函数
*/
handleBlankTap() {
console.log("scroll-view 区域被点击,准备关闭输入框");
this.closeInput();
},
/**
* ✅ 核心修改:处理点击评论项的逻辑
*/
handleCommentTap(e: WechatMiniprogram.TouchEvent) {
const wasShowing = this.data.showReplyInput;
const tappedCommentId = e.currentTarget.dataset.item?.id;
// 步骤 1: 立刻关闭当前可能已打开的输入框
this.closeInput();
// 步骤 2: 使用延时来重新打开输入框
// 这是一个技巧,确保关闭操作完成后再执行打开操作,避免冲突
setTimeout(() => {
const commentData = e.currentTarget.dataset.item;
if (commentData) {
// 如果刚刚输入框是开着的,并且点的是同一个评论,那么我们其实是想关闭它,而不是重新打开
if (wasShowing && this.data.currentReplyId === tappedCommentId) {
return;
}
this.setData({
showReplyInput: true,
currentReplyId: commentData.id,
currentReplyUsername: commentData.username || '匿名用户',
replyContent: '',
isSubmitDisabled: true,
});
}
}, 50); // 50毫秒的延时足够
},
/**
* ✅ 核心修改:处理点击主评论入口的逻辑
*/
handleTapPrimaryCommentInput() {
// 同样,先关闭
this.closeInput();
// 延时后打开
setTimeout(() => {
this.setData({
showReplyInput: true,
currentReplyId: null,
currentReplyUsername: '',
});
}, 50);
},
/**
* ✅ 修改:提交时智能判断是“回复”还是“新评论”
*/
handleReplySubmit() {
if (this.data.isSubmitDisabled) return;
const { replyContent, currentReplyId, cardReviewData } = this.data;
const articleId = cardReviewData.article_id;
// 根据 currentReplyId 是否有值来判断
if (currentReplyId) {
// --- 这是回复 ---
console.log(`[提交回复] 内容: "${replyContent}" | 回复目标ID: ${currentReplyId} | 文章ID: ${articleId}`);
// 在此处对接您的【回复】API
// wx.request({ url: ..., data: { content: replyContent, parent_id: currentReplyId, article_id: articleId } })
} else {
// --- 这是新的一级评论 ---
console.log(`[提交新评论] 内容: "${replyContent}" | 文章ID: ${articleId}`);
// 在此处对接您的【发表新评论】API
// wx.request({ url: ..., data: { content: replyContent, article_id: articleId } })
}
wx.showToast({ title: '评论成功(模拟)', icon: 'success' });
// 提交后清空并隐藏输入框
this.setData({
showReplyInput: false,
currentReplyId: null,
currentReplyUsername: '',
replyContent: '',
isSubmitDisabled: true
});
// (可选) 成功后可以延时刷新评论列表
// setTimeout(() => {
// this.getCardDetail(this.properties.id);
// }, 1000);
},
/**
* 实时同步输入框内容
*/
noop() {
return;
},
/**
* 输入框失去焦点
*/
handleInputBlur() {
// 目前主要靠 handleBlankTap 和 handleReplySubmit 隐藏,此函数可留空
},
/**
* 提交回复
*/
handleReplySubmit() {
const { replyContent, currentReplyId } = this.data;
if (!replyContent.trim()) {
wx.showToast({ title: '回复内容不能为空', icon: 'none' });
return;
}
console.log(`[准备发送] 回复内容: "${replyContent}" | 回复目标评论ID: ${currentReplyId}`);
// --- 在此处对接您的后端API发送回复请求 ---
// wx.request({ ... });
wx.showToast({ title: '回复成功(模拟)', icon: 'success' });
this.setData({
showReplyInput: false,
currentReplyId: '',
currentReplyUsername: '',
replyContent: ''
});
},
onShareAppMessage: function (res) {
// 1. 获取文评数据,添加容错(避免数据为空导致报错)
const { cardReviewData = {} } = this.data;
const {
article_title = "未命名文评", // 文评标题默认值
total_voters = 0, // 总投票人数默认值
vote_type = "单选", // 投票类型(单选/多选)默认值
options = [] // 选项列表默认空数组
} = cardReviewData;
// 2. 处理选项数据:拼接“选项名+投票数”如“方案A15票/方案B13票”
const optionText = options.length
? options.map(opt => `${opt.name || '未命名选项'}${opt.votes || 0}`).join('/')
: "暂无选项数据"; // 无选项时的默认提示
// 3. 拼接最终分享标题(整合所有关键信息)
const shareTitle = `${article_title}(已有${total_voters}人投票:${optionText}`;
// 4. 返回分享配置不设置imageUrl微信会自动使用页面截图作为分享图
return {
title: shareTitle, // 整合了关键数据的标题
path: `/pages/articledetail/articledetail?id=${cardReviewData.article_id || ''}`, // 跳转路径容错避免id为空
// 不设置imageUrl微信会自动截取当前页面顶部区域作为分享图确保页面顶部有文评相关内容
success: (res) => {
console.log('分享成功', res);
// 可选:分享成功后给用户提示
wx.showToast({ title: '分享成功', icon: 'success', duration: 1500 });
},
fail: (res) => {
console.log('分享失败', res);
wx.showToast({ title: '分享失败', icon: 'none', duration: 1500 });
}
};
},
// *** 递归计算评论总数的辅助函数 ***
calculateTotalComments(comments: Comment[]): number {
let count = 0;
for (const comment of comments) {
// 1. 计入当前评论(顶级评论或回复)
count++;
// 2. 递归计入子评论
if (comment.replies && comment.replies.length > 0) {
count += this.calculateTotalComments(comment.replies);
}
}
return count;
},
getCardDetail(id: string) {
console.log("正在加载文章ID:", id);
const articleId = parseInt(id, 10);
if (isNaN(articleId) || articleId <= 0) {
this.setData({
loading: false,
error: "文章ID格式错误"
});
return;
}
// 开始加载,重置状态
this.setData({ loading: true, error: null });
this.getComments(articleId)
.then(commentsData => {
// --- ✅ 核心修复逻辑在这里 ---
// 无论 commentsData 是有内容的数组还是空数组,都是成功状态
console.log("获取到 commentsData:", commentsData);
const totalCount = this.calculateTotalComments(commentsData);
console.log(`文章ID ${articleId} 评论获取成功,总评论数: ${totalCount}`);
this.setData({
comments: commentsData || [], // 确保 comments 是一个数组
totalCommentsCount: totalCount,
loading: false,
error: null // 确保成功时error 状态被清空
});
})
.catch(error => {
// 只有当 getComments promise被 reject (即网络或服务器真出错了) 才会进入这里
console.error(`获取评论失败123: ${error.message}`);
this.setData({
loading: false,
// 这里只显示真实的错误信息
error: error.message || "获取评论失败,请检查网络"
});
});
},
getComments(articleId: number): Promise<Comment[]> {
return new Promise((resolve, reject) => {
wx.request({
url: `${envConfig.apiBaseUrl}/comment/get`,
method: "POST",
data: {
articleId: articleId
},
success: (res) => {
console.log('[getComments] 收到服务器原始响应:', res.data);
const response = res.data as CommentResponse;
// ✅ 核心修复 1: 同时接受 data 为数组 或 data 为 null 的情况
if (response.success && (Array.isArray(response.data) || response.data === null)) {
console.log('[getComments] 数据校验成功, resolve 数据');
// ✅ 核心修复 2: 如果 data 是 null我们将其视为空数组 [] 返回
// 这样可以保证后续的 .then 代码接收到的永远是一个数组
resolve(response.data || []);
} else {
// 只有当服务器明确告知失败, 或数据格式不对时, 才 reject
console.error('[getComments] 服务器返回数据异常, reject 错误');
reject(new Error(response.message || "服务器返回数据格式不正确"));
}
},
fail: (err) => {
console.error('[getComments] 网络请求失败, reject 错误:', err);
reject(new Error("网络请求失败,请检查连接"));
}
});
});
},
hasReplies(replies:Comment[]) {
console.log('检测子评论:', {
isArray: Array.isArray(replies), // 是否为数组
length: replies ? replies.length : '无数据', // 长度
rawData: replies // 原始数据
});
// 双重校验1. 是标准数组 2. 长度大于0避免空数组或非数组
return Array.isArray(replies) && replies.length > 0;
},
loadCardReviewData(id: string) {
// 1. 检查 app 实例是否存在
const app = getApp<IApp>();
if (!app) {
console.error('无法获取 App 实例,请确保 App 已启动。');
return;
}
// 2. 检查 app.processedCardsData 是否存在且是数组
if (Array.isArray(app.processedCardsData) && app.processedCardsData.length > 0) {
console.log('app详情页获取到的文章数据', app.globalData.processedCardsData);
// ⭐️ 核心修复:先对字符串 id 进行 trim(),再进行 Number() 转换
const targetId = Number(id.trim());
// 打印调试信息,确认转换后的数值
console.log(`调试properties.id(原始): "${id}"`);
console.log(`调试:目标查找 ID (数值, trim后): ${targetId}`);
const cardData = app.processedCardsData.find(item => {
// 确保比较的是数值 === 数值
return item.article_id === targetId;
});
if (cardData) {
console.log(`从 app.ts 找到文评数据ID: ${id}`);
this.setData({
cardReviewData: cardData
});
console.log("当前界面中cardReviewData:",this.data.cardReviewData)
} else {
console.log(`app.ts 中未找到 ID 为 ${id} 的文评数据。`);
}
} else {
console.warn('App.processedCardsData 不存在或格式错误,无法加载文评数据。');
if (app.processedCardsData === undefined || app.processedCardsData === null) {
console.warn('确认 app.ts 中 processedCardsData 是否在 App({}) 根部定义!');
}
}
},
formatIsoDateTime(isoString: string): string {
console.log("调用formatIsoDateTime:",isoString)
if (!isoString) return '';
// 处理ISO格式字符串如2025-09-25T17:00:08+08:00
const date = new Date(isoString);
// 提取日期部分
const year = date.getFullYear();
const month = this.padZero(date.getMonth() + 1); // 月份从0开始
const day = this.padZero(date.getDate());
// 提取时间部分
const hours = this.padZero(date.getHours());
const minutes = this.padZero(date.getMinutes());
// 格式化为 xx年xx月xx日 xx:xx
return `${year}${month}${day}${hours}:${minutes}`;
},
// 数字补零辅助函数
padZero(num: number): string {
return num < 10 ? `0${num}` : num.toString();
},
// 修复核心:单选/多选逻辑(基于单个 cardReviewData 对象)
selectOption(e: WechatMiniprogram.TouchEvent) {
const { optionId, cardId } = e.currentTarget.dataset as {
optionId: number;
cardId: number
};
const { cardReviewData, maxSelectCount } = this.data;
// 1. 基础校验:卡片不存在/投票已结束,直接返回
if (!cardReviewData || cardReviewData.article_id !== cardId) {
console.error("未找到对应的卡片数据");
return;
}
if (cardReviewData.is_ended) {
wx.showToast({ title: '投票已结束,无法选择', icon: 'none' });
return;
}
// 2. 深拷贝卡片数据(避免直接修改原数据)
const newCardReviewData = JSON.parse(JSON.stringify(cardReviewData));
// 确保 selectedOptions 始终是数组
if (!Array.isArray(newCardReviewData.selectedOptions)) {
newCardReviewData.selectedOptions = [];
}
const currentOptions = newCardReviewData.options || [];
// 3. 多选逻辑(保持不变)
if (newCardReviewData.vote_type === 'multiple') {
const optionIndex = currentOptions.findIndex((o: any) => o.id === optionId);
if (optionIndex === -1) return;
const isAlreadySelected = newCardReviewData.selectedOptions.includes(optionId);
if (isAlreadySelected) {
// 取消选择移除已选ID + 取消选项选中状态
newCardReviewData.selectedOptions = newCardReviewData.selectedOptions
.filter(id => id !== optionId);
currentOptions[optionIndex].isSelected = false;
} else {
// 新增选择:检查最大选择数限制
if (newCardReviewData.selectedOptions.length >= maxSelectCount) {
wx.showToast({
title: `最多只能选择${maxSelectCount}`,
icon: 'none'
});
return;
}
newCardReviewData.selectedOptions.push(optionId);
currentOptions[optionIndex].isSelected = true;
}
// 4. 单选逻辑(核心修改:支持取消选择)
} else if (newCardReviewData.vote_type === 'single') {
// 4.1 判断当前点击的选项是否已选中
const isCurrentSelected = newCardReviewData.selectedOptions.includes(optionId);
if (isCurrentSelected) {
// 👉 情况1已选中 → 取消选择(清空所有选中状态)
currentOptions.forEach((option: any) => {
option.isSelected = false; // 取消所有选项的选中状态
});
newCardReviewData.selectedOptions = []; // 清空已选列表
} else {
// 👉 情况2未选中 → 切换选择(取消其他选项,选中当前)
currentOptions.forEach((option: any) => {
option.isSelected = (option.id === optionId); // 只选中当前选项
});
newCardReviewData.selectedOptions = [optionId]; // 更新已选列表
}
}
// 5. 同步更新页面数据
this.setData({ cardReviewData: newCardReviewData }, () => {
console.log(`卡片 ${cardId} 当前选中项:`, newCardReviewData.selectedOptions);
});
},
submitVote(e: WechatMiniprogram.TouchEvent) {
const { cardId } = e.currentTarget.dataset as { cardId: number };
const { cardReviewData } = this.data;
// 从全局获取用户ID需确保App.globalData中已存储uid若从接口获取需调整
console.log("全局用户uid:",getApp<IApp>().globalData.userInfo.uid)
const uid = getApp<IApp>().globalData.userInfo.uid;
// 1. 基础校验:数据不存在/投票已结束/无用户ID
if (!cardReviewData || cardReviewData.article_id !== cardId) {
wx.showToast({ title: '未找到投票数据', icon: 'none' });
return;
}
if (cardReviewData.is_ended) {
wx.showToast({ title: '投票已结束,无法提交', icon: 'none' });
return;
}
if (!uid) {
wx.showToast({ title: '用户未登录,请先登录', icon: 'none' });
return;
}
// 2. 处理已选选项:确保是数组且非空
const selectedOptions = Array.isArray(cardReviewData.selectedOptions)
? cardReviewData.selectedOptions
: [];
if (selectedOptions.length === 0) {
wx.showToast({ title: '请至少选择一个选项', icon: 'none' });
return;
}
// 3. 核心:根据投票类型动态构建请求参数
let voteParams: {
uid: string;
articleId: number;
optionId?: number;
optionIds?: number[]
} = {
uid: uid, // 从全局获取用户ID
articleId: cardId // 投票卡片ID与article_id一致
};
// 单选添加optionId取已选列表第一个值因单选最多一个
if (cardReviewData.vote_type === 'single') {
voteParams.optionId = selectedOptions[0]; // 单选已选列表仅1项直接取第0个
}
// 多选添加optionIds直接传入已选数组
else if (cardReviewData.vote_type === 'multiple') {
voteParams.optionIds = selectedOptions;
}
console.log("投票发送:",voteParams)
// 4. 发送投票请求(替换为实际接口地址,适配后端要求)
wx.request({
url: `${envConfig.apiBaseUrl}/article/vote`, // 实际投票提交接口
method: 'POST',
header: {
'Content-Type': 'application/json',
// 若需Token验证添加Token头根据后端要求
// 'Authorization': `Bearer ${getApp<IApp>().globalData.token}`
},
data: voteParams, // 动态构建的差异化参数
success: (res) => {
// 假设后端返回格式:{ success: boolean; message: string }
const response = res.data;
if (response.success) {
wx.showToast({ title: '投票提交成功', icon: 'success' });
// 投票成功后:可更新卡片状态(如标记为已投票、禁用选项)
this.setData({
['cardReviewData.is_voted']: true, // 标记用户已投票(需卡片数据支持)
['cardReviewData.is_ended']: response.data?.is_ended || cardReviewData.is_ended // 若后端返回投票结束状态,同步更新
});
// // 通知父组件投票成功(如需)
// this.triggerEvent('voteSuccess', {
// cardId: cardId,
// selectedOptions: selectedOptions,
// voteParams: voteParams,
// timestamp: Date.now()
// });
} else {
wx.showToast({ title: response.message || '投票提交失败', icon: 'none' });
console.error('投票提交失败:', response.message);
}
},
fail: (err) => {
wx.showToast({ title: '网络错误,投票失败', icon: 'none' });
console.error('投票请求失败:', err);
}
});
},
},
})