Initial project

This commit is contained in:
Timi
2025-07-08 16:28:40 +08:00
parent 03cf3638d7
commit fb1438c393
44 changed files with 13002 additions and 129 deletions

View File

@ -0,0 +1,241 @@
<template>
<div v-if="items" class="tui-comment-list">
<div class="comment" v-for="comment in items" :key="comment.id">
<!-- 主评论 -->
<div class="user">
<!-- 主评论头像 -->
<template v-if="comment.user">
<a
:href="`/user/space/${comment.userId}`"
target="_blank"
v-popup:config="popupUserStore.config"
@mouseenter="popupUserStore.user.value = comment.user"
>
<img
class="avatar"
:class="(<any>ImageType)[comment.user.profile.avatarType]"
:src="UserAPI.getAvatarURL(comment.user.profile)"
alt="主评论头像"
/>
</a>
<a
class="name pink selectable"
:href="`/user/space/${comment.userId}`"
v-text="comment.user.name"
v-popup:config="popupUserStore.config"
@mouseenter="popupUserStore.user.value = comment.user"
></a>
</template>
<!-- 游客的 -->
<template v-else>
<img class="avatar ir-pixelated" :src="defAvatarURL" alt="头像" />
<div class="name gray selectable clip-text" v-text="comment.nick"></div>
</template>
</div>
<div class="content">
<!-- 主评论内容 -->
<markdown-view class="value" :content="comment.content" />
<div class="reply-to">
<a class="button" href="javascript:" @click="replyTo(comment)">回复</a>
<span
class="light-gray"
v-text="Time.toPassedDate(comment.createdAt)"
v-popup:text="Time.toDateTime(comment.createdAt)"
></span>
</div>
<!-- 子评论 -->
<comment-reply-list :comment="comment" @reply="replyTo" />
<t-pagination
class="pages"
v-if="6 < comment.repliesLength"
:total="comment.repliesLength"
:pageSize="6"
:showPageSize="false"
v-model:current="comment.repliesCurrent"
@change="() => doFetchReply(comment)"
>
<template #total-content>
<div class="total" v-text="`共 ${comment.repliesLength} 条回复`"></div>
</template>
</t-pagination>
<!-- 回复表单此表单跟随页面上激活的回复对象 -->
<comment-reply-form
v-if="canReply && comment.id && activeReply === comment.id"
:commentId="comment.id"
:replyId="replyId"
:replyToNick="replyToNick"
@submit="onSubmitReply"
@cancel="activeReply = -1"
/>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import {
CommentAPI,
CommentReply,
CommentReplyView,
CommentView,
CommonAPI,
ImageType,
MarkdownView,
SettingKey,
SettingMapper,
Time,
UserAPI
} from "timi-web";
import { CommentReplyForm, CommentReplyList } from "~/components";
import { popupUserStore } from "timi-tdesign-pc";
defineOptions({
name: "CommentList"
});
const props = withDefaults(defineProps<{
items: CommentView[],
canReply?: boolean,
}>(), {
canReply: true
});
const { items, canReply } = toRefs(props);
// 默认头像
const defAvatarURL = ref();
onMounted(async () => {
const res = JSON.parse(SettingMapper.getValue(SettingKey.PUBLIC_RESOURCES) as string);
defAvatarURL.value = CommonAPI.getAttachmentReadAPI(res.user.avatar);
});
// 激活的回复评论下标
const activeReply = ref();
const replyId = ref();
// 被回复昵称
const replyToNick = ref();
/**
* 获取回复
*
* @param comment 所属评论
*/
async function doFetchReply(comment: CommentView) {
// 组件下标以 1 开始,分页参数以 0 开始
comment.repliesPage.index = comment.repliesCurrent - 1;
const result = await CommentAPI.pageReply(comment.repliesPage);
comment.replies.length = 0;
comment.replies.push(...result.list);
}
/**
* 触发回复表单(表单会跟随评论)
*
* @param comment 跟随的评论
* @param replyTo 被回复对象
*/
async function replyTo(comment: CommentView, replyTo?: CommentReplyView) {
activeReply.value = comment.id;
if (replyTo) {
// 被回复对象
replyToNick.value = replyTo.sender?.name || replyTo.senderNick;
replyId.value = replyTo.id;
} else {
replyToNick.value = comment.user?.name || comment.nick;
}
}
function onSubmitReply(reply: CommentReply) {
activeReply.value = -1;
const comment = items.value.find(comment => comment.id === reply.commentId)!;
comment.repliesLength++;
comment.repliesCurrent = Math.ceil(comment.repliesLength / 6); // 上取整最后一页
doFetchReply(comment);
}
</script>
<style lang="less" scoped>
.tui-comment-list {
font-size: 13px;
line-height: 1.5;
.comment {
display: flex;
line-height: 1.5;
border-bottom: 2px solid #CDDEF0;
.user {
width: 120px;
padding: 16px 0;
text-align: center;
background: #E4EFFA;
transition: .5s var(--tui-bezier);
.avatar {
width: 64px;
border: 1px solid #FBC7D4;
margin: 0 auto 10px auto;
padding: 3px;
background: #FFF;
transition: .5s var(--tui-bezier);
}
}
.content {
width: calc(100% - 120px);
transition: .5s var(--tui-bezier);
.value {
width: calc(100% - 2rem);
padding: .5rem;
min-height: 92px;
}
.pages {
padding: 4px 4px 4px 1rem;
border-top: 1px solid #E4EFFA;
.total {
flex: 1;
}
}
.reply-to {
display: flex;
padding-right: .5rem;
justify-content: end;
> a {
margin-right: .5rem;
}
}
}
}
}
@media screen and (max-width: 650px) {
.tui-comment-list {
.comment {
.user {
width: 60px;
padding: 8px 0;
transition: .5s var(--tui-bezier);
.avatar {
width: 32px;
margin: 0 auto 4px auto;
padding: 2px;
transition: .5s var(--tui-bezier);
}
}
.content {
width: calc(100% - 60px);
transition: .5s var(--tui-bezier);
}
}
}
}
</style>