595 lines
23 KiB
TypeScript
595 lines
23 KiB
TypeScript
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);
|
||
}
|
||
});
|
||
},
|
||
|
||
|
||
|
||
|
||
|
||
},
|
||
|
||
|
||
}) |