Initial project
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
import { Comment } from "timi-web";
|
||||
|
||||
/**
|
||||
* emits
|
||||
*/
|
||||
export interface Emits {
|
||||
(event: "submit"): Promise<void>;
|
||||
}
|
||||
|
||||
export const useHandler = (emit: Emits) => {
|
||||
|
||||
const onSubmit = async (comment: Comment) => await emit("submit");
|
||||
|
||||
return {
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import view from "./index.vue";
|
||||
import { Toolkit } from "timi-web";
|
||||
|
||||
export const CommentForm = Toolkit.withInstall(view);
|
||||
export default CommentForm;
|
||||
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<t-form class="tui-comment-form">
|
||||
<t-form-item label="昵称" name="nick">
|
||||
<t-input
|
||||
class="nick"
|
||||
v-model="formData.data.nick"
|
||||
:disabled="userStore.isLogged()"
|
||||
placeholder="昵称"
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item label="评论" name="content">
|
||||
<markdown-editor v-model:data="formData.data.content" />
|
||||
</t-form-item>
|
||||
<t-form-item label="验证码" name="captcha">
|
||||
<t-input class="captcha" v-model="formData.captcha" placeholder="" />
|
||||
<captcha
|
||||
ref="captchaRef"
|
||||
class="captcha-img"
|
||||
:api="CommonAPI.getCaptchaAPI()"
|
||||
:width="90"
|
||||
:height="28"
|
||||
:from="CaptchaFrom.COMMENT"
|
||||
/>
|
||||
<t-button
|
||||
type="submit"
|
||||
:loading="doSubmitting"
|
||||
:disabled="doSubmitting"
|
||||
@click="doSubmit"
|
||||
>提交</t-button>
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
Captcha,
|
||||
CaptchaData,
|
||||
CaptchaFrom,
|
||||
Comment,
|
||||
CommentAPI,
|
||||
CommentBizType,
|
||||
CommonAPI,
|
||||
MarkdownEditor,
|
||||
userStore
|
||||
} from "timi-web";
|
||||
import { Emits, useHandler } from "~/components/comment/form/emits";
|
||||
|
||||
defineOptions({
|
||||
name: "CommentForm"
|
||||
});
|
||||
|
||||
const emits = defineEmits<Emits>();
|
||||
const { onSubmit } = useHandler(emits);
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
bizType: CommentBizType,
|
||||
bizId: number,
|
||||
}>(), {});
|
||||
const { bizType, bizId } = toRefs(props);
|
||||
|
||||
const captchaRef = ref<{ update: () => void}>();
|
||||
const formData = reactive<CaptchaData<Comment>>({
|
||||
from: CaptchaFrom.COMMENT,
|
||||
captcha: "",
|
||||
data: {
|
||||
bizType: bizType.value,
|
||||
bizId: bizId.value,
|
||||
nick: "",
|
||||
content: ""
|
||||
}
|
||||
});
|
||||
const doSubmitting = ref(false);
|
||||
|
||||
async function doSubmit() {
|
||||
doSubmitting.value = true;
|
||||
|
||||
const loginUser = userStore.loginUser;
|
||||
if (userStore.isLogged() && loginUser.user) {
|
||||
formData.data.nick = loginUser.user.name;
|
||||
formData.data.userId = loginUser.user.id;
|
||||
}
|
||||
await CommentAPI.create(formData);
|
||||
formData.captcha = "";
|
||||
formData.data.content = "";
|
||||
captchaRef.value?.update();
|
||||
|
||||
doSubmitting.value = false;
|
||||
|
||||
await onSubmit(formData.data);
|
||||
}
|
||||
|
||||
// 登录用户
|
||||
const updateNick = () => formData.data.nick = userStore.loginUser?.user?.name || "";
|
||||
watch(userStore.loginUser, updateNick);
|
||||
onMounted(updateNick);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tui-comment-form {
|
||||
padding: 1rem;
|
||||
|
||||
.nick {
|
||||
width: 12rem;
|
||||
}
|
||||
|
||||
.captcha {
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.captcha-img {
|
||||
margin: 0 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
import { CommentReply } from "timi-web";
|
||||
|
||||
/**
|
||||
* emits
|
||||
*/
|
||||
export interface Emits {
|
||||
(event: "cancel"): Promise<void>;
|
||||
(event: "submit", reply: CommentReply): Promise<void>;
|
||||
}
|
||||
|
||||
export const useHandler = (emit: Emits) => {
|
||||
|
||||
const onCancel = async () => await emit("cancel");
|
||||
const onSubmit = async (reply: CommentReply) => await emit("submit", reply);
|
||||
|
||||
return {
|
||||
onCancel,
|
||||
onSubmit
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import view from "./index.vue";
|
||||
import { Toolkit } from "timi-web";
|
||||
|
||||
export const CommentReplyForm = Toolkit.withInstall(view);
|
||||
export default CommentReplyForm;
|
||||
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<t-form class="tui-comment-reply-form">
|
||||
<t-form-item label="昵称" name="nick">
|
||||
<t-input
|
||||
ref="nickRef"
|
||||
class="nick"
|
||||
v-model="formData.data.senderNick"
|
||||
placeholder="昵称"
|
||||
:disabled="userStore.isLogged()"
|
||||
/>
|
||||
<span v-if="replyToNick" class="gray selectable clip-text" v-text="` 回复 ${replyToNick}`"></span>
|
||||
</t-form-item>
|
||||
<t-form-item label="评论" name="content">
|
||||
<markdown-editor ref="contentRef" v-model:data="formData.data.content" />
|
||||
</t-form-item>
|
||||
<t-form-item label="验证码" name="captcha">
|
||||
<div class="captcha-box">
|
||||
<div>
|
||||
<t-input class="captcha" v-model="formData.captcha" placeholder="" />
|
||||
<captcha
|
||||
ref="captchaRef"
|
||||
class="captcha-img"
|
||||
:api="CommonAPI.getCaptchaAPI()"
|
||||
:width="90"
|
||||
:height="28"
|
||||
:from="CaptchaFrom.COMMENT_REPLY"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<t-button
|
||||
class="submit"
|
||||
type="submit"
|
||||
:loading="doSubmitting"
|
||||
:disabled="doSubmitting"
|
||||
@click="doSubmit"
|
||||
>提交</t-button>
|
||||
<t-button theme="default" @click="onCancel">取消</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
Captcha,
|
||||
CaptchaData,
|
||||
CaptchaFrom,
|
||||
CommentAPI,
|
||||
CommentReply,
|
||||
CommonAPI,
|
||||
MarkdownEditor,
|
||||
Scroller,
|
||||
userStore
|
||||
} from "timi-web";
|
||||
import { Emits, useHandler } from "~/components/comment/form/reply/emits";
|
||||
|
||||
defineOptions({
|
||||
name: "CommentReplyForm"
|
||||
});
|
||||
|
||||
const emits = defineEmits<Emits>();
|
||||
const { onCancel, onSubmit } = useHandler(emits);
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
commentId: number,
|
||||
replyId?: number,
|
||||
replyToNick?: string,
|
||||
}>(), {});
|
||||
const { commentId, replyId, replyToNick } = toRefs(props);
|
||||
|
||||
const doSubmitting = ref(false);
|
||||
const nickRef = ref();
|
||||
const contentRef = ref();
|
||||
|
||||
const formData = reactive<CaptchaData<CommentReply>>({
|
||||
from: CaptchaFrom.COMMENT_REPLY,
|
||||
captcha: "",
|
||||
data: {
|
||||
commentId: commentId.value,
|
||||
replyId: replyId.value,
|
||||
senderNick: "",
|
||||
content: ""
|
||||
}
|
||||
});
|
||||
|
||||
/** 提交回复 */
|
||||
async function doSubmit() {
|
||||
doSubmitting.value = true;
|
||||
|
||||
formData.data.commentId = commentId.value;
|
||||
formData.data.replyId = replyId.value;
|
||||
const loginUser = userStore.loginUser;
|
||||
if (userStore.isLogged() && loginUser.user) {
|
||||
formData.data.senderNick = loginUser.user.name;
|
||||
formData.data.senderId = loginUser.user.id;
|
||||
}
|
||||
await CommentAPI.createReply(formData);
|
||||
formData.captcha = "";
|
||||
await onSubmit(formData.data);
|
||||
|
||||
doSubmitting.value = false;
|
||||
}
|
||||
|
||||
async function focus() {
|
||||
await nextTick();
|
||||
let focusEl: HTMLElement;
|
||||
if (userStore.isLogged()) {
|
||||
focusEl = nickRef.value[0].textArea;
|
||||
} else {
|
||||
focusEl = contentRef.value[0].inputRef;
|
||||
}
|
||||
focusEl.focus();
|
||||
Scroller.toElement(focusEl);
|
||||
}
|
||||
|
||||
// 登录用户
|
||||
const updateNick = () => formData.data.senderNick = userStore.loginUser?.user?.name || "";
|
||||
watch(userStore.loginUser, updateNick);
|
||||
onMounted(updateNick);
|
||||
|
||||
defineExpose({
|
||||
focus
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tui-comment-reply-form {
|
||||
padding: 1rem 1rem 1rem 0;
|
||||
|
||||
.nick {
|
||||
width: 12rem;
|
||||
}
|
||||
|
||||
.captcha-box {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.captcha {
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.captcha-img {
|
||||
margin: 0 1rem;
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 650px) {
|
||||
.tui-comment-reply-form {
|
||||
|
||||
.captcha-box {
|
||||
|
||||
> div:first-child {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
import view from "./index.vue";
|
||||
import { Toolkit } from "timi-web";
|
||||
|
||||
export const Comment = Toolkit.withInstall(view);
|
||||
export default Comment;
|
||||
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="canComment || (pageResult && 0 < pageResult.total)"
|
||||
class="tui-comment"
|
||||
:class="{ 'can-comment': canComment }"
|
||||
>
|
||||
<span ref="titleAnchorRef"></span>
|
||||
<h4
|
||||
class="title cur-pointer"
|
||||
@click="Scroller.toElement(titleAnchorRef!)"
|
||||
>评论</h4>
|
||||
<comment-form
|
||||
ref="commentFormRef"
|
||||
v-if="canComment"
|
||||
:bizType="bizType" :bizId="bizId"
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
<t-pagination
|
||||
v-if="pageResult && 16 < pageResult.total"
|
||||
class="pages top"
|
||||
:total="pageResult.total"
|
||||
:pageSize="16"
|
||||
:showPageSize="false"
|
||||
v-model:current="pageCurrent"
|
||||
:onCurrentChange="doFetch"
|
||||
>
|
||||
<template #total-content>
|
||||
<div class="total" v-text="`共 ${pageResult.total} 条评论`"></div>
|
||||
</template>
|
||||
</t-pagination>
|
||||
<comment-list class="list" v-if="pageResult" :items="pageResult.list" />
|
||||
<t-pagination
|
||||
v-if="pageResult && 16 < pageResult.total"
|
||||
class="pages bottom"
|
||||
:total="pageResult.total"
|
||||
:pageSize="16"
|
||||
:showPageSize="false"
|
||||
v-model:current="pageCurrent"
|
||||
:onCurrentChange="doFetch"
|
||||
>
|
||||
<template #total-content>
|
||||
<div class="total" v-text="`共 ${pageResult.total} 条评论`"></div>
|
||||
</template>
|
||||
</t-pagination>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
CommentAPI,
|
||||
CommentBizType,
|
||||
CommentPage,
|
||||
CommentReplyBizType,
|
||||
CommentView,
|
||||
PageResult,
|
||||
Scroller
|
||||
} from "timi-web";
|
||||
import { CommentForm, CommentList } from "~/components";
|
||||
|
||||
// 评论总成
|
||||
defineOptions({
|
||||
name: "Comment"
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
bizType: CommentBizType,
|
||||
bizId: number,
|
||||
canComment?: boolean,
|
||||
titleStickyOffset?: number,
|
||||
}>(), {
|
||||
canComment: true,
|
||||
titleStickyOffset: 0
|
||||
});
|
||||
const { bizType, bizId, canComment } = toRefs(props);
|
||||
|
||||
// 数据列表
|
||||
const page = reactive<CommentPage>({
|
||||
bizType: bizType.value,
|
||||
bizId: bizId.value,
|
||||
index: 0,
|
||||
size: 16
|
||||
});
|
||||
// 当前页
|
||||
const pageCurrent = ref(1);
|
||||
const pageResult = ref<PageResult<CommentView>>();
|
||||
async function doFetch() {
|
||||
page.index = pageCurrent.value - 1;
|
||||
pageResult.value = await CommentAPI.page(page);
|
||||
for (let i = 0; i < pageResult.value.list.length; i++) {
|
||||
const item = pageResult.value.list[i];
|
||||
item.repliesCurrent = 1;
|
||||
item.repliesPage = {
|
||||
bizId: item.id!,
|
||||
bizType: CommentReplyBizType.COMMENT,
|
||||
index: 0,
|
||||
size: 6
|
||||
}
|
||||
}
|
||||
}
|
||||
onMounted(doFetch);
|
||||
|
||||
const titleAnchorRef = ref<HTMLSpanElement>();
|
||||
|
||||
/** 提交评论 */
|
||||
async function onSubmit() {
|
||||
page.index = 0;
|
||||
await doFetch();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tui-comment {
|
||||
|
||||
.title {
|
||||
top: v-bind("titleStickyOffset + 'px'");
|
||||
margin: 0;
|
||||
padding: .5rem 1rem;
|
||||
z-index: 2;
|
||||
position: sticky;
|
||||
background: #E4EFFA;
|
||||
box-shadow: 0 -1px 0 var(--tui-light-gray);
|
||||
border-bottom: var(--tui-border);
|
||||
}
|
||||
|
||||
&.can-comment {
|
||||
|
||||
.title {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.list {
|
||||
border-top: 2px solid #CDDEF0;
|
||||
}
|
||||
}
|
||||
|
||||
.pages {
|
||||
padding: 4px 4px 4px 1rem;
|
||||
|
||||
&.top {
|
||||
border-top: 1px solid #E4EFFA;
|
||||
}
|
||||
|
||||
&.bottom {
|
||||
border-bottom: 1px solid #E4EFFA;
|
||||
}
|
||||
|
||||
.total {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* emits
|
||||
*/
|
||||
export interface Emits {
|
||||
}
|
||||
|
||||
export const useHandler = (emit: Emits) => {
|
||||
|
||||
return {
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import view from "./index.vue";
|
||||
import { Toolkit } from "timi-web";
|
||||
|
||||
export const CommentList = Toolkit.withInstall(view);
|
||||
export default CommentList;
|
||||
@@ -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>
|
||||
@@ -0,0 +1,17 @@
|
||||
import { CommentReplyView, CommentView } from "timi-web";
|
||||
|
||||
/**
|
||||
* emits
|
||||
*/
|
||||
export interface Emits {
|
||||
(event: "reply", comment: CommentView, replyTo?: CommentReplyView): Promise<void>;
|
||||
}
|
||||
|
||||
export const useHandler = (emit: Emits) => {
|
||||
|
||||
const onReply = async (comment: CommentView, replyTo?: CommentReplyView) => await emit("reply", comment, replyTo);
|
||||
|
||||
return {
|
||||
onReply,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import view from "./index.vue";
|
||||
import { Toolkit } from "timi-web";
|
||||
|
||||
export const CommentReplyList = Toolkit.withInstall(view);
|
||||
export default CommentReplyList;
|
||||
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<div class="tui-comment-reply-list">
|
||||
<div v-if="comment && comment.replies" class="reply" v-for="reply in comment.replies" :key="reply.id">
|
||||
<div class="header">
|
||||
<!-- 发送者头像 -->
|
||||
<div class="sender-avatar">
|
||||
<a
|
||||
v-if="reply.sender"
|
||||
:href="`/user/space/${reply.senderId}`"
|
||||
v-popup:config="popupUserStore.config"
|
||||
@mouseenter="popupUserStore.user.value = reply.sender"
|
||||
>
|
||||
<img
|
||||
:class="(<any>ImageType)[reply.sender.profile.avatarType]"
|
||||
:src="UserAPI.getAvatarURL(reply.sender.profile)"
|
||||
alt="回复发送者头像"
|
||||
/>
|
||||
</a>
|
||||
<img v-else class="ir-pixelated" :src="defAvatarURL" alt="回复发送者头像" />
|
||||
</div>
|
||||
<!-- 被回复用户 -->
|
||||
<a
|
||||
v-if="reply.sender"
|
||||
class="pink bold selectable"
|
||||
:href="`/user/space/${reply.senderId}`"
|
||||
target="_blank"
|
||||
v-text="reply.sender.name"
|
||||
v-popup:config="popupUserStore.config"
|
||||
@mouseenter="popupUserStore.user.value = reply.sender"
|
||||
></a>
|
||||
<span
|
||||
v-else
|
||||
class="gray selectable"
|
||||
v-text="reply.senderNick"
|
||||
></span>
|
||||
<!-- 非回复层主时才显示被回复用户 -->
|
||||
<template v-if="!!reply.replyId">
|
||||
<span> 回复 </span>
|
||||
<!-- 被回复用户 -->
|
||||
<a
|
||||
v-if="reply.receiver"
|
||||
class="pink bold selectable"
|
||||
:href="`/user/space/${reply.receiverId}`"
|
||||
target="_blank"
|
||||
v-text="reply.receiver.name"
|
||||
v-popup:config="popupUserStore.config"
|
||||
@mouseenter="popupUserStore.user.value = reply.receiver"
|
||||
></a>
|
||||
<span
|
||||
v-else
|
||||
class="gray selectable"
|
||||
v-text="reply.receiverNick"
|
||||
></span>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 回复内容 -->
|
||||
<markdown-view class="content" :content="reply.content" />
|
||||
<!-- 其他回复信息 -->
|
||||
<div class="reply-to">
|
||||
<a href="javascript:" @click="onReply(comment, reply)">回复</a>
|
||||
<span
|
||||
class="light-gray"
|
||||
v-text="Time.toPassedDate(reply.createdAt)"
|
||||
v-popup:text="Time.toDateTime(reply.createdAt)"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
CommentView,
|
||||
CommonAPI,
|
||||
ImageType,
|
||||
MarkdownView,
|
||||
PublicResources,
|
||||
SettingKey,
|
||||
SettingMapper,
|
||||
Time,
|
||||
UserAPI
|
||||
} from "timi-web";
|
||||
import { Emits, useHandler } from "~/components/comment/list/reply/emits";
|
||||
import { popupUserStore } from "~/store/popupUser";
|
||||
|
||||
defineOptions({
|
||||
name: "CommentReplyList"
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
comment: CommentView,
|
||||
}>(), {});
|
||||
const { comment } = toRefs(props);
|
||||
|
||||
const emits = defineEmits<Emits>();
|
||||
const { onReply } = useHandler(emits);
|
||||
|
||||
// 默认头像
|
||||
const defAvatarURL = ref();
|
||||
onMounted(async () => {
|
||||
const res = JSON.parse(SettingMapper.getValue(SettingKey.PUBLIC_RESOURCES) as string) as PublicResources;
|
||||
defAvatarURL.value = CommonAPI.getAttachmentReadAPI(res.user.avatar);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tui-comment-reply-list {
|
||||
|
||||
.reply {
|
||||
padding: 6px;
|
||||
|
||||
&:nth-child(n + 2) {
|
||||
border-top: 1px #d7d7d7 dotted;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top: 2px solid #E4EFFA;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.sender-avatar {
|
||||
border: 1px solid #FBC7D4;
|
||||
padding: 1px;
|
||||
margin-right: .5rem;
|
||||
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
transition: .5s var(--tui-bezier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.reply-to {
|
||||
display: flex;
|
||||
padding-right: 0;
|
||||
justify-content: end;
|
||||
|
||||
> a {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@media screen and (max-width: 650px) {
|
||||
.tui-comment-reply-list {
|
||||
|
||||
.reply {
|
||||
padding: 3px;
|
||||
transition: .5s var(--tui-bezier);
|
||||
|
||||
.header {
|
||||
|
||||
.sender-avatar {
|
||||
|
||||
img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
transition: .5s var(--tui-bezier);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user