✨ Comments
This commit is contained in:
parent
450c1e4450
commit
f9438b4d89
@ -25,7 +25,6 @@ func contextComment() *services.PostTypeContext {
|
|||||||
func listComment(c *fiber.Ctx) error {
|
func listComment(c *fiber.Ctx) error {
|
||||||
take := c.QueryInt("take", 0)
|
take := c.QueryInt("take", 0)
|
||||||
offset := c.QueryInt("offset", 0)
|
offset := c.QueryInt("offset", 0)
|
||||||
noReact := c.QueryBool("noReact", false)
|
|
||||||
|
|
||||||
alias := c.Params("postId")
|
alias := c.Params("postId")
|
||||||
|
|
||||||
@ -37,7 +36,7 @@ func listComment(c *fiber.Ctx) error {
|
|||||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := mx.ListComment(item.ID, take, offset, noReact)
|
data, err := mx.ListComment(item.ID, take, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.smartsheep.studio/hydrogen/interactive/pkg/database"
|
"code.smartsheep.studio/hydrogen/interactive/pkg/database"
|
||||||
@ -94,7 +95,6 @@ func listPost(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
func reactPost(c *fiber.Ctx) error {
|
func reactPost(c *fiber.Ctx) error {
|
||||||
user := c.Locals("principal").(models.Account)
|
user := c.Locals("principal").(models.Account)
|
||||||
id, _ := c.ParamsInt("articleId", 0)
|
|
||||||
|
|
||||||
var data struct {
|
var data struct {
|
||||||
Symbol string `json:"symbol" validate:"required"`
|
Symbol string `json:"symbol" validate:"required"`
|
||||||
@ -107,16 +107,40 @@ func reactPost(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
mx := c.Locals(postContextKey).(*services.PostTypeContext)
|
mx := c.Locals(postContextKey).(*services.PostTypeContext)
|
||||||
|
|
||||||
item, err := mx.Get(uint(id), true)
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
reaction := models.Reaction{
|
reaction := models.Reaction{
|
||||||
Symbol: data.Symbol,
|
Symbol: data.Symbol,
|
||||||
Attitude: data.Attitude,
|
Attitude: data.Attitude,
|
||||||
AccountID: user.ID,
|
AccountID: user.ID,
|
||||||
ArticleID: &item.ID,
|
}
|
||||||
|
|
||||||
|
postType := c.Params("postType")
|
||||||
|
alias := c.Params("postId")
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var res models.Feed
|
||||||
|
|
||||||
|
switch postType {
|
||||||
|
case "moments":
|
||||||
|
err = database.C.Model(&models.Moment{}).Where("id = ?", alias).Select("id").First(&res).Error
|
||||||
|
case "articles":
|
||||||
|
err = database.C.Model(&models.Article{}).Where("id = ?", alias).Select("id").First(&res).Error
|
||||||
|
case "comments":
|
||||||
|
err = database.C.Model(&models.Comment{}).Where("id = ?", alias).Select("id").First(&res).Error
|
||||||
|
default:
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "comment must belongs to a resource")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("belongs to resource was not found: %v", err))
|
||||||
|
} else {
|
||||||
|
switch postType {
|
||||||
|
case "moments":
|
||||||
|
reaction.MomentID = &res.ID
|
||||||
|
case "articles":
|
||||||
|
reaction.ArticleID = &res.ID
|
||||||
|
case "comments":
|
||||||
|
reaction.CommentID = &res.ID
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if positive, reaction, err := mx.React(reaction); err != nil {
|
if positive, reaction, err := mx.React(reaction); err != nil {
|
||||||
|
@ -20,7 +20,7 @@ func (v *PostTypeContext) ListComment(id uint, take int, offset int, noReact ...
|
|||||||
userTable := viper.GetString("database.prefix") + "accounts"
|
userTable := viper.GetString("database.prefix") + "accounts"
|
||||||
if err := v.Tx.
|
if err := v.Tx.
|
||||||
Table(table).
|
Table(table).
|
||||||
Select("*, ? as model_type", "comments").
|
Select("*, ? as model_type", "comment").
|
||||||
Where(v.ColumnName+"_id = ?", id).
|
Where(v.ColumnName+"_id = ?", id).
|
||||||
Joins(fmt.Sprintf("INNER JOIN %s as author ON author_id = author.id", userTable)).
|
Joins(fmt.Sprintf("INNER JOIN %s as author ON author_id = author.id", userTable)).
|
||||||
Limit(take).Offset(offset).Find(&items).Error; err != nil {
|
Limit(take).Offset(offset).Find(&items).Error; err != nil {
|
||||||
|
57
pkg/views/src/components/comments/CommentList.vue
Normal file
57
pkg/views/src/components/comments/CommentList.vue
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="loading" class="text-center flex items-center justify-center">
|
||||||
|
<v-progress-circular indeterminate />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="flex flex-col gap-2 mt-3">
|
||||||
|
<div v-for="(item, idx) in props.comments" class="text-sm">
|
||||||
|
<post-item :item="item" @update:item="val => updateItem(idx, val)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { request } from "@/scripts/request"
|
||||||
|
import { reactive, ref } from "vue"
|
||||||
|
import PostItem from "@/components/posts/PostItem.vue"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
comments: any[]
|
||||||
|
model: any
|
||||||
|
alias: any
|
||||||
|
}>()
|
||||||
|
const emits = defineEmits(["update:comments"])
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
|
const pagination = reactive({ page: 0, pageSize: 10, total: 0 })
|
||||||
|
|
||||||
|
async function readComments() {
|
||||||
|
loading.value = true
|
||||||
|
const res = await request(
|
||||||
|
`/api/p/${props.model}/${props.alias}/comments?` +
|
||||||
|
new URLSearchParams({
|
||||||
|
take: pagination.pageSize.toString(),
|
||||||
|
offset: (pagination.page * pagination.pageSize).toString()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
if (res.status !== 200) {
|
||||||
|
error.value = await res.text()
|
||||||
|
} else {
|
||||||
|
error.value = null
|
||||||
|
const data = await res.json()
|
||||||
|
pagination.total = data["total"]
|
||||||
|
emits("update:comments", data["data"])
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
readComments()
|
||||||
|
|
||||||
|
function updateItem(idx: number, data: any) {
|
||||||
|
const comments = JSON.parse(JSON.stringify(props.comments));
|
||||||
|
comments[idx] = data;
|
||||||
|
emits("update:comments", comments);
|
||||||
|
}
|
||||||
|
</script>
|
20
pkg/views/src/components/posts/CommentContent.vue
Normal file
20
pkg/views/src/components/posts/CommentContent.vue
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<article class="prose prose-comment" v-html="parseContent(props.item.content)" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import dompurify from "dompurify";
|
||||||
|
import { parse } from "marked";
|
||||||
|
|
||||||
|
const props = defineProps<{ item: any }>();
|
||||||
|
|
||||||
|
function parseContent(src: string): string {
|
||||||
|
return dompurify().sanitize(parse(src) as string);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.prose.prose-comment p {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,6 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-card :loading="props.loading">
|
|
||||||
<template #text>
|
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<div>
|
<div>
|
||||||
<v-avatar
|
<v-avatar
|
||||||
@ -14,8 +12,8 @@
|
|||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<div class="font-bold">{{ props.item?.author.nick }}</div>
|
<div class="font-bold">{{ props.item?.author.nick }}</div>
|
||||||
|
|
||||||
<div v-if="props.item?.model_type === 'article'" class="text-xs text-grey-darken-4 mb-2">Published an
|
<div v-if="props.item?.model_type === 'article'" class="text-xs text-grey-darken-4 mb-2">
|
||||||
article
|
Published an article
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<component :is="renderer[props.item?.model_type]" v-bind="props" />
|
<component :is="renderer[props.item?.model_type]" v-bind="props" />
|
||||||
@ -30,31 +28,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</v-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Component } from "vue";
|
import type { Component } from "vue"
|
||||||
import ArticleContent from "@/components/posts/ArticleContent.vue";
|
import ArticleContent from "@/components/posts/ArticleContent.vue"
|
||||||
import MomentContent from "@/components/posts/MomentContent.vue";
|
import MomentContent from "@/components/posts/MomentContent.vue"
|
||||||
import PostReaction from "@/components/posts/PostReaction.vue";
|
import CommentContent from "@/components/posts/CommentContent.vue"
|
||||||
|
import PostReaction from "@/components/posts/PostReaction.vue"
|
||||||
|
|
||||||
const props = defineProps<{ item: any, brief?: boolean, loading?: boolean }>();
|
const props = defineProps<{ item: any; brief?: boolean }>()
|
||||||
const emits = defineEmits(["update:item"]);
|
const emits = defineEmits(["update:item"])
|
||||||
|
|
||||||
const renderer: { [id: string]: Component } = {
|
const renderer: { [id: string]: Component } = {
|
||||||
article: ArticleContent,
|
article: ArticleContent,
|
||||||
moment: MomentContent
|
moment: MomentContent,
|
||||||
};
|
comment: CommentContent
|
||||||
|
}
|
||||||
|
|
||||||
function updateReactions(symbol: string, num: number) {
|
function updateReactions(symbol: string, num: number) {
|
||||||
const item = JSON.parse(JSON.stringify(props.item));
|
const item = JSON.parse(JSON.stringify(props.item))
|
||||||
if (item.reaction_list.hasOwnProperty(symbol)) {
|
if (item.reaction_list == null) {
|
||||||
item.reaction_list[symbol] += num;
|
item.reaction_list = {}
|
||||||
} else {
|
|
||||||
item.reaction_list[symbol] = num;
|
|
||||||
}
|
}
|
||||||
emits("update:item", item);
|
if (item.reaction_list.hasOwnProperty(symbol)) {
|
||||||
|
item.reaction_list[symbol] += num
|
||||||
|
} else {
|
||||||
|
item.reaction_list[symbol] = num
|
||||||
|
}
|
||||||
|
emits("update:item", item)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -7,7 +7,11 @@
|
|||||||
<v-infinite-scroll :items="props.posts" :onLoad="props.loader">
|
<v-infinite-scroll :items="props.posts" :onLoad="props.loader">
|
||||||
<template v-for="(item, idx) in props.posts" :key="item">
|
<template v-for="(item, idx) in props.posts" :key="item">
|
||||||
<div class="mb-3 px-1">
|
<div class="mb-3 px-1">
|
||||||
|
<v-card>
|
||||||
|
<template #text>
|
||||||
<post-item brief :item="item" @update:item="val => updateItem(idx, val)" />
|
<post-item brief :item="item" @update:item="val => updateItem(idx, val)" />
|
||||||
|
</template>
|
||||||
|
</v-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</v-infinite-scroll>
|
</v-infinite-scroll>
|
||||||
|
@ -43,7 +43,7 @@ const emits = defineEmits(["update"]);
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
size?: string,
|
size?: string,
|
||||||
readonly?: boolean,
|
readonly?: boolean,
|
||||||
model: string,
|
model: any,
|
||||||
item: any,
|
item: any,
|
||||||
reactions: { [id: string]: number }
|
reactions: { [id: string]: number }
|
||||||
}>();
|
}>();
|
||||||
@ -63,7 +63,7 @@ const status = reactive({ added: false, removed: false });
|
|||||||
const error = ref<string | null>(null);
|
const error = ref<string | null>(null);
|
||||||
|
|
||||||
async function reactPost(symbol: string, attitude: number) {
|
async function reactPost(symbol: string, attitude: number) {
|
||||||
const res = await request(`/api/${props.model}/${props.item?.id}/react`, {
|
const res = await request(`/api/p/${props.model}/${props.item?.id}/react`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Authorization": `Bearer ${getAtk()}`, "Content-Type": "application/json" },
|
headers: { "Authorization": `Bearer ${getAtk()}`, "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ symbol, attitude })
|
body: JSON.stringify({ symbol, attitude })
|
||||||
|
@ -11,6 +11,17 @@
|
|||||||
<v-divider class="mt-5 mx-[-16px] border-opacity-50" />
|
<v-divider class="mt-5 mx-[-16px] border-opacity-50" />
|
||||||
|
|
||||||
<article-content :item="post" content-only />
|
<article-content :item="post" content-only />
|
||||||
|
|
||||||
|
<v-divider class="my-5 mx-[-16px] border-opacity-50" />
|
||||||
|
|
||||||
|
<div class="px-3">
|
||||||
|
<post-reaction
|
||||||
|
:item="post"
|
||||||
|
:model="route.params.postType"
|
||||||
|
:reactions="post?.reaction_list ?? {}"
|
||||||
|
@update="updateReactions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</article>
|
</article>
|
||||||
</v-card>
|
</v-card>
|
||||||
@ -18,14 +29,8 @@
|
|||||||
|
|
||||||
<div class="aside sticky top-0 w-full h-fit md:min-w-[280px]">
|
<div class="aside sticky top-0 w-full h-fit md:min-w-[280px]">
|
||||||
<v-card title="Comments">
|
<v-card title="Comments">
|
||||||
<v-list density="compact">
|
|
||||||
</v-list>
|
|
||||||
</v-card>
|
|
||||||
|
|
||||||
<v-card title="Reactions" class="mt-3">
|
|
||||||
<div class="px-[1rem] pb-[0.825rem] mt-[-12px]">
|
<div class="px-[1rem] pb-[0.825rem] mt-[-12px]">
|
||||||
<post-reaction :item="post" :model="route.params.postType" :reactions="post?.reaction_list ?? {}"
|
<comment-list v-model:comments="comments" :model="route.params.postType" :alias="route.params.alias" />
|
||||||
@update="updateReactions" />
|
|
||||||
</div>
|
</div>
|
||||||
</v-card>
|
</v-card>
|
||||||
</div>
|
</div>
|
||||||
@ -33,37 +38,40 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref } from "vue"
|
||||||
import { request } from "@/scripts/request";
|
import { request } from "@/scripts/request"
|
||||||
import { useRoute } from "vue-router";
|
import ArticleContent from "@/components/posts/ArticleContent.vue"
|
||||||
import ArticleContent from "@/components/posts/ArticleContent.vue";
|
import PostReaction from "@/components/posts/PostReaction.vue"
|
||||||
import PostReaction from "@/components/posts/PostReaction.vue";
|
import CommentList from "@/components/comments/CommentList.vue"
|
||||||
|
import { useRoute } from "vue-router"
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false)
|
||||||
const error = ref<string | null>(null);
|
const error = ref<string | null>(null)
|
||||||
const post = ref<any>(null);
|
|
||||||
|
|
||||||
const route = useRoute();
|
const post = ref<any>(null)
|
||||||
|
const comments = ref<any[]>([])
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
async function readPost() {
|
async function readPost() {
|
||||||
loading.value = true;
|
loading.value = true
|
||||||
const res = await request(`/api/p/${route.params.postType}/${route.params.alias}?`);
|
const res = await request(`/api/p/${route.params.postType}/${route.params.alias}`)
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
error.value = await res.text();
|
error.value = await res.text()
|
||||||
} else {
|
} else {
|
||||||
error.value = null;
|
error.value = null
|
||||||
post.value = await res.json();
|
post.value = await res.json()
|
||||||
}
|
}
|
||||||
loading.value = false;
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
readPost();
|
readPost()
|
||||||
|
|
||||||
function updateReactions(symbol: string, num: number) {
|
function updateReactions(symbol: string, num: number) {
|
||||||
if (post.value.reaction_list.hasOwnProperty(symbol)) {
|
if (post.value.reaction_list.hasOwnProperty(symbol)) {
|
||||||
post.value.reaction_list[symbol] += num;
|
post.value.reaction_list[symbol] += num
|
||||||
} else {
|
} else {
|
||||||
post.value.reaction_list[symbol] = num;
|
post.value.reaction_list[symbol] = num
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
Loading…
Reference in New Issue
Block a user