refactor(views): 重构多个视图组件代码结构,优化类型定义和逻辑组织

feat(services): 新增文章分页查询方法,支持按状态筛选文章

style(styles): 调整主布局样式,优化分页组件显示效果

docs(README): 更新API文档,完善服务模块说明和类型定义

fix(components): 修复左侧模块点击属性时使用错误字段名的问题

chore(package): 移除未使用的依赖项,清理项目依赖

perf(layouts): 优化主布局组件性能,拆分功能模块,减少重复计算

test(views): 为分页组件添加基础测试用例

build: 更新构建配置,优化生产环境打包

ci: 调整CI配置,添加类型检查步骤
This commit is contained in:
qingfeng1121
2025-11-14 15:30:29 +08:00
parent 4ae0ff7c2a
commit 1dc5bdd93f
16 changed files with 1883 additions and 2456 deletions

View File

@@ -131,6 +131,8 @@ import { ElMessage, ElForm } from 'element-plus'
import { useGlobalStore } from '@/store/globalStore'
import { formatDate } from '@/utils/dateUtils'
// ============================== 组件初始化 ==============================
// 定义组件属性
const props = defineProps({
comments: {
@@ -138,65 +140,37 @@ const props = defineProps({
default: null
}
})
const globalStore = useGlobalStore()
const messageBoardData = ref([]) // 留言板留言articleid为空的主留言及其回复
const loading = ref(false)
const submitting = ref(false)
const replyingTo = ref({ id: null, nickname: '', content: '' })
const formRef = ref()
// 验证码相关状态
const captchaHint = ref('')
const captchaAnswer = ref('')
const showCaptchaHint = ref(false)
// 全局状态管理
const globalStore = useGlobalStore()
// ============================== 响应式数据 ==============================
// 留言板数据状态
const messageBoardData = ref([]) // 留言板留言articleid为空的主留言及其回复
const loading = ref(false) // 加载状态
const submitting = ref(false) // 提交状态
const replyingTo = ref({ id: null, nickname: '', content: '' }) // 正在回复的信息
const formRef = ref() // 表单引用
// 验证码相关状态
const captchaHint = ref('') // 验证码提示
const captchaAnswer = ref('') // 验证码答案
const showCaptchaHint = ref(false) // 是否显示验证码提示
// 表单数据
const form = reactive({
parentid: null,
replyid: null,
articleid: null,
content: '',
nickname: '',
email: '',
captcha: ''
parentid: null, // 父留言ID
replyid: null, // 被回复留言ID
articleid: null, // 文章ID
content: '', // 评论内容
nickname: '', // 昵称
email: '', // 邮箱
captcha: '' // 验证码
})
// 生成简单验证码
const generateCaptcha = () => {
// 随机选择数学题或字符验证码
const isMathCaptcha = Math.random() > 0.5
// ============================== 表单验证规则 ==============================
if (isMathCaptcha) {
// 简单数学题:加法或减法
const num1 = Math.floor(Math.random() * 10) + 1
const num2 = Math.floor(Math.random() * 10) + 1
const operator = Math.random() > 0.5 ? '+' : '-'
let answer
if (operator === '+') {
answer = num1 + num2
} else {
// 确保减法结果为正
const larger = Math.max(num1, num2)
const smaller = Math.min(num1, num2)
captchaHint.value = `${larger} - ${smaller} = ?`
captchaAnswer.value = (larger - smaller).toString()
return
}
captchaHint.value = `${num1} ${operator} ${num2} = ?`
captchaAnswer.value = answer.toString()
} else {
// 简单字符验证码
const chars = 'ABCDEFGHIJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz123456789'
let captcha = ''
for (let i = 0; i < 4; i++) {
captcha += chars.charAt(Math.floor(Math.random() * chars.length))
}
captchaHint.value = captcha
captchaAnswer.value = captcha.toLowerCase()
}
}
// 表单验证规则
const rules = {
content: [
{ required: true, message: '请输入评论内容', trigger: 'blur' },
@@ -237,42 +211,109 @@ const rules = {
]
}
// ============================== 验证码模块 ==============================
// 获取Gravatar头像URL
/**
* 生成验证码
* 随机选择生成数学题验证码或字符验证码
*/
const generateCaptcha = () => {
// 随机选择数学题或字符验证码
const isMathCaptcha = Math.random() > 0.5
if (isMathCaptcha) {
generateMathCaptcha()
} else {
generateCharCaptcha()
}
}
/**
* 生成数学题验证码
* 包含简单的加法或减法运算
*/
const generateMathCaptcha = () => {
// 简单数学题:加法或减法
const num1 = Math.floor(Math.random() * 10) + 1
const num2 = Math.floor(Math.random() * 10) + 1
const operator = Math.random() > 0.5 ? '+' : '-'
if (operator === '+') {
const answer = num1 + num2
captchaHint.value = `${num1} ${operator} ${num2} = ?`
captchaAnswer.value = answer.toString()
} else {
// 确保减法结果为正
const larger = Math.max(num1, num2)
const smaller = Math.min(num1, num2)
captchaHint.value = `${larger} - ${smaller} = ?`
captchaAnswer.value = (larger - smaller).toString()
}
}
/**
* 生成字符验证码
* 随机生成4位大小写字母和数字的组合
*/
const generateCharCaptcha = () => {
const chars = 'ABCDEFGHIJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz123456789'
let captcha = ''
for (let i = 0; i < 4; i++) {
captcha += chars.charAt(Math.floor(Math.random() * chars.length))
}
captchaHint.value = captcha
captchaAnswer.value = captcha.toLowerCase()
}
// ============================== UI辅助模块 ==============================
/**
* 获取Gravatar头像URL
* @param {string} email - 用户邮箱
* @returns {string|null} - 头像URL或null
*/
const getAvatarUrl = (email) => {
if (!email) return null;
// 简单验证邮箱格式
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (!emailRegex.test(email)) return null;
// 使用邮箱的MD5哈希这里简化处理实际项目中应该使用md5库
// 注意在实际项目中应该使用正确的MD5哈希函数
return `https://www.gravatar.com/avatar/${email}?d=404&s=40`;
}
// 获取名字的首字母
/**
* 获取名字的首字母
* 用于生成字母头像
* @param {string} name - 用户昵称
* @returns {string} - 首字母或问号
*/
const getInitialLetter = (name) => {
if (!name || typeof name !== 'string') return '?';
// 移除可能的@回复前缀
const cleanName = name.replace(/^.+@\s*/, '');
// 获取第一个字符
const firstChar = cleanName.charAt(0).toUpperCase();
return firstChar;
// 获取第一个字符并转为大写
return cleanName.charAt(0).toUpperCase();
}
// 获取首字母头像的样式
/**
* 获取首字母头像的样式
* 根据名字生成一致的颜色
* @param {string} name - 用户昵称
* @returns {Object} - 包含背景色和文字颜色的样式对象
*/
const getLetterAvatarStyle = (name) => {
// 颜色映射表,根据名字生成一致的颜色
// 预定义的颜色数组
const colors = [
'#4A90E2', '#50E3C2', '#F5A623', '#D0021B', '#9013FE',
'#B8E986', '#BD10E0', '#50E3C2', '#417505', '#7ED321',
'#BD10E0', '#F8E71C', '#8B572A', '#9B9B9B', '#4A4A4A'
];
// 根据名字生成一个一致的颜色索引
// 根据名字生成一致的颜色索引
let hash = 0;
if (name) {
for (let i = 0; i < name.length; i++) {
@@ -287,195 +328,258 @@ const getLetterAvatarStyle = (name) => {
};
}
// 从后端获取留言列表
// ============================== 数据处理模块 ==============================
/**
* 处理留言数据,构建留言与回复的层级结构
* @param {Array} messages - 原始留言数据
* @returns {Array} - 处理后的留言数据(包含回复数组)
*/
const processMessageData = (messages) => {
// 为主留言添加replies数组
const allMessagesWithReplies = messages.map(msg => ({
...msg,
replies: []
}))
// 分离主留言和回复
const mainMessages = []
const replies = []
allMessagesWithReplies.forEach(msg => {
if (msg.parentid && msg.parentid > 0) {
replies.push(msg)
} else {
mainMessages.push(msg)
}
})
// 将回复添加到对应的主留言中并处理@回复显示
processRepliesForMainMessages(mainMessages, replies)
return mainMessages
}
/**
* 处理回复数据,将其添加到对应的主留言中
* @param {Array} mainMessages - 主留言数组
* @param {Array} replies - 回复数组
*/
const processRepliesForMainMessages = (mainMessages, replies) => {
replies.forEach(reply => {
// 找到父留言
const parentMsg = mainMessages.find(msg => msg.messageid === reply.parentid)
if (parentMsg) {
// 处理@回复的显示名称
processReplyDisplayName(reply, replies)
parentMsg.replies.push(reply)
}
})
}
/**
* 处理回复的显示名称
* 如果是回复其他回复,添加@标记
* @param {Object} reply - 回复对象
* @param {Array} allReplies - 所有回复数组
*/
const processReplyDisplayName = (reply, allReplies) => {
if (reply.replyid) {
const repliedMsg = allReplies.find(msg => msg.messageid === reply.replyid)
if (repliedMsg) {
reply.displayName = `${reply.nickname}@ ${repliedMsg.nickname}`
} else {
reply.displayName = reply.nickname
}
} else {
reply.displayName = reply.nickname
}
}
// ============================== API调用模块 ==============================
/**
* 从后端获取留言列表
* 根据articleid决定获取文章留言还是全局留言
*/
const fetchMessages = async () => {
try {
loading.value = true
let res = null
// 优先使用props传递的articleid其次使用globalStore中的数据
let articleid = props.comments || null
if (!articleid) {
// 安全获取文章ID如果globalStore中没有articleInfo则返回null
const articleData = globalStore.getValue('articleInfo')
articleid = (articleData && typeof articleData === 'object' && 'articleid' in articleData) ? articleData.articleid : null
}
// 获取文章ID优先使用props,其次使用全局状态)
const articleid = getArticleId()
form.articleid = articleid
// 根据是否有文章ID选择不同的API调用
if (articleid) {
res = await messageService.getMessagesByArticleId(articleid)
} else {
res = await messageService.getAllMessages()
// 过滤掉articleid不为空的留言只保留articleid为空或不存在的留言
if (res && res.data) {
res.data = res.data.filter(msg => !msg.articleid || msg.articleid === '')
}
}
res = await (articleid
? messageService.getMessagesByArticleId(articleid)
: fetchAllMessages()
)
// 验证响应结果
if (!res || !res.data) {
console.warn('未获取到留言数据')
messageBoardData.value = []
handleEmptyResponse()
return
}
const allMessages = res.data
// 处理所有留言为主留言添加replies数组
const allMessagesWithReplies = allMessages.map(msg => ({
...msg,
replies: []
}))
// 分离主留言和回复
const mainMessages = []
const replies = []
allMessagesWithReplies.forEach(msg => {
if (msg.parentid && msg.parentid > 0) {
replies.push(msg)
} else {
mainMessages.push(msg)
}
})
// 将回复添加到对应的主留言中
replies.forEach(reply => {
// 找到父留言
const parentMsg = mainMessages.find(msg => msg.messageid === reply.parentid)
if (parentMsg) {
// 处理@回复的显示名称
if (reply.replyid) {
const repliedMsg = replies.find(msg => msg.messageid === reply.replyid)
if (repliedMsg) {
reply.displayName = `${reply.nickname}@ ${repliedMsg.nickname}`
} else {
reply.displayName = reply.nickname
}
} else {
reply.displayName = reply.nickname
}
parentMsg.replies.push(reply)
}
})
// 更新留言板数据
messageBoardData.value = mainMessages
// 处理留言数据
messageBoardData.value = processMessageData(res.data)
} catch (error) {
console.error('获取留言列表失败:', error)
ElMessage.error('获取留言失败,请稍后重试')
messageBoardData.value = [] // 出错时清空数据,避免显示错误内容
handleFetchError(error)
} finally {
loading.value = false
}
}
// 处理回复
const handleReply = (msg, reply) => {
// 检查是否是回复模式
if (msg !== null) {
// 回复模式
form.replyid = reply.messageid
form.parentid = msg.messageid
} else {
// 普通回复模式
form.replyid = null
form.parentid = reply.messageid
/**
* 获取文章ID
* 优先级props.comments > globalStore中的articleInfo
* @returns {number|null} - 文章ID或null
*/
const getArticleId = () => {
let articleid = props.comments || null
if (!articleid) {
// 安全获取文章ID如果globalStore中没有articleInfo则返回null
const articleData = globalStore.getValue('articleInfo')
articleid = (articleData && typeof articleData === 'object' && 'articleid' in articleData)
? articleData.articleid
: null
}
return articleid
}
/**
* 获取所有留言并过滤
* 只保留articleid为空或不存在的全局留言
* @returns {Promise} - API响应
*/
const fetchAllMessages = async () => {
const res = await messageService.getAllMessages()
// 过滤掉articleid不为空的留言只保留articleid为空或不存在的留言
if (res && res.data) {
res.data = res.data.filter(msg => !msg.articleid || msg.articleid === '')
}
return res
}
/**
* 处理空响应
*/
const handleEmptyResponse = () => {
console.warn('未获取到留言数据')
messageBoardData.value = []
}
/**
* 处理获取留言错误
* @param {Error} error - 错误对象
*/
const handleFetchError = (error) => {
console.error('获取留言列表失败:', error)
ElMessage.error('获取留言失败,请稍后重试')
messageBoardData.value = [] // 出错时清空数据,避免显示错误内容
}
/**
* 提交留言
* @returns {Promise<void>}
*/
const submitMessage = async () => {
const res = await messageService.saveMessage({
content: form.content,
nickname: form.nickname,
email: form.email,
parentid: form.parentid,
replyid: form.replyid,
articleid: form.articleid
})
return res
}
/**
* 点赞留言
* @param {number} messageId - 留言ID
* @returns {Promise} - API响应
*/
const likeMessage = async (messageId) => {
return await messageService.likeMessage(messageId)
}
/**
* 删除留言
* @param {number} messageId - 留言ID
* @returns {Promise} - API响应
*/
const deleteMessage = async (messageId) => {
return await messageService.deleteMessage(messageId)
}
// ============================== 交互操作模块 ==============================
/**
* 处理回复操作
* @param {Object|null} msg - 主留言对象null表示直接回复主留言
* @param {Object} reply - 被回复的留言对象
*/
const handleReply = (msg, reply) => {
// 设置回复相关表单数据
setupReplyFormData(msg, reply)
// 记录正在回复的信息
replyingTo.value = {
id: reply.messageid,
nickname: reply.nickname || '匿名用户',
content: reply.content
}
// 滚动到输入框
scrollToInput()
}
/**
* 设置回复表单数据
* @param {Object|null} msg - 主留言对象
* @param {Object} reply - 被回复的留言对象
*/
const setupReplyFormData = (msg, reply) => {
if (msg !== null) {
// 回复模式:回复某条回复
form.replyid = reply.messageid
form.parentid = msg.messageid
} else {
// 普通回复模式:回复主留言
form.replyid = null
form.parentid = reply.messageid
}
}
/**
* 滚动到输入框区域
*/
const scrollToInput = () => {
setTimeout(() => {
document.querySelector('.message-form-section')?.scrollIntoView({ behavior: 'smooth' })
document.querySelector('.message-form-section')?.scrollIntoView({
behavior: 'smooth'
})
}, 100)
}
// 取消回复
/**
* 取消回复
*/
const cancelReply = () => {
replyingTo.value = { id: null, nickname: '', content: '' }
form.replyid = null
form.content = ''
}
// 监听articleid变化重新加载留言
watch(() => props.comments, (newVal) => {
if (newVal) {
fetchMessages()
}
}, { immediate: false })
// 组件挂载时获取留言列表
onMounted(() => {
fetchMessages()
generateCaptcha() // 页面加载时生成验证码
})
const onSubmit = async () => {
if (!formRef.value) return;
// 表单验证
await formRef.value.validate((valid) => {
if (!valid) {
ElMessage.warning('请检查表单填写是否正确');
throw new Error('表单验证失败');
}
});
console.log('提交留言表单:', form)
try {
submitting.value = true
if (form.parentid) {
// 回复模式
const res = await messageService.saveMessage({
content: form.content,
nickname: form.nickname,
email: form.email,
parentid: form.parentid,
replyid: form.replyid,
articleid: form.articleid
})
if (res.success) {
ElMessage.success('回复成功')
fetchMessages() // 重新获取列表
resetForm()
cancelReply()
} else {
ElMessage.error('回复失败:' + (res.message || '未知错误'))
}
} else {
// 普通留言
const res = await messageService.saveMessage({
content: form.content,
nickname: form.nickname,
email: form.email,
articleid: form.articleid
})
if (res.success) {
ElMessage.success('留言成功')
fetchMessages() // 重新获取列表
resetForm()
} else {
ElMessage.error('留言失败:' + (res.message || '未知错误'))
}
}
} catch (error) {
console.error('提交失败:', error)
ElMessage.error('网络错误,请稍后重试')
} finally {
submitting.value = false
}
}
// 重置表单
/**
* 重置表单
*/
const resetForm = () => {
form.replyid = null
form.content = ''
@@ -487,19 +591,79 @@ const resetForm = () => {
replyingTo.value = { id: null, nickname: '', content: '' }
}
const post_comment_reply_cancel = () => {
form.content = ''
form.replyid = null
replyingTo.value = { id: null, nickname: '', content: '' }
/**
* 提交评论
*/
const onSubmit = async () => {
if (!formRef.value) return;
try {
// 表单验证
await validateForm()
console.log('提交留言表单:', form)
submitting.value = true
// 发送留言并处理结果
await handleMessageSubmission()
} catch (error) {
console.error('提交失败:', error)
ElMessage.error('网络错误,请稍后重试')
} finally {
submitting.value = false
}
}
// 处理点赞
/**
* 验证表单
* @returns {Promise<void>}
*/
const validateForm = async () => {
return new Promise((resolve, reject) => {
formRef.value.validate((valid) => {
if (!valid) {
ElMessage.warning('请检查表单填写是否正确');
reject(new Error('表单验证失败'));
} else {
resolve();
}
});
});
}
/**
* 处理留言提交
* @returns {Promise<void>}
*/
const handleMessageSubmission = async () => {
const res = await submitMessage()
if (res.success) {
// 提交成功
ElMessage.success(form.parentid ? '回复成功' : '留言成功')
await fetchMessages() // 重新获取列表
resetForm()
// 如果是回复模式,取消回复状态
if (form.parentid) {
cancelReply()
}
} else {
// 提交失败
ElMessage.error(`${form.parentid ? '回复' : '留言'}失败:${res.message || '未知错误'}`)
}
}
/**
* 处理点赞操作
* @param {Object} msg - 留言对象
*/
const handleLike = async (msg) => {
try {
// 显示加载状态或禁用按钮
// 显示加载状态
msg.isLiking = true
const res = await messageService.likeMessage(msg.messageid)
const res = await likeMessage(msg.messageid)
if (res.success && res.data) {
// 更新点赞数
@@ -515,19 +679,23 @@ const handleLike = async (msg) => {
msg.isLiking = false
}
}
// 处理删除
const handleDelete = async (msg) => {
/**
* 处理删除操作
* @param {number} messageId - 留言ID
*/
const handleDelete = async (messageId) => {
// 确认删除
if (!confirm('确定删除吗?')) {
return
}
try {
// 显示加载状态或禁用按钮
// msg.isDeleting = true
if (!confirm('确定删除吗?')) {
return
}
const res = await messageService.deleteMessage(msg.messageid)
const res = await deleteMessage(messageId)
if (res.success) {
// 从列表中移除
messageBoardData.value = messageBoardData.value.filter(item => item.messageid !== msg.messageid)
messageBoardData.value = messageBoardData.value.filter(item => item.messageid !== messageId)
ElMessage.success('删除成功')
} else {
ElMessage.error('删除失败:' + (res.message || '未知错误'))
@@ -535,10 +703,32 @@ const handleDelete = async (msg) => {
} catch (error) {
console.error('删除失败:', error)
ElMessage.error('网络错误,请稍后重试')
} finally {
// msg.isDeleting = false
}
}
/**
* 另一个取消回复的方法(冗余但保留以兼容现有代码)
*/
const post_comment_reply_cancel = () => {
form.content = ''
form.replyid = null
replyingTo.value = { id: null, nickname: '', content: '' }
}
// ============================== 生命周期和监听器 ==============================
// 监听articleid变化重新加载留言
watch(() => props.comments, (newVal) => {
if (newVal) {
fetchMessages()
}
}, { immediate: false })
// 组件挂载时初始化
onMounted(() => {
fetchMessages() // 获取留言列表
generateCaptcha() // 生成验证码
})
</script>
<style scoped lang="scss">