Comments

This commit is contained in:
LittleSheep 2024-03-06 23:31:22 +08:00
parent 450c1e4450
commit f9438b4d89
9 changed files with 194 additions and 81 deletions

View File

@ -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())
} }

View File

@ -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 {

View File

@ -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 {

View 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>

View 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>

View File

@ -1,60 +1,61 @@
<template> <template>
<v-card :loading="props.loading"> <div class="flex gap-3">
<template #text> <div>
<div class="flex gap-3"> <v-avatar
<div> color="grey-lighten-2"
<v-avatar icon="mdi-account-circle"
color="grey-lighten-2" class="rounded-card"
icon="mdi-account-circle" :src="props.item?.author.avatar"
class="rounded-card" />
:src="props.item?.author.avatar" </div>
/>
</div>
<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>
<component :is="renderer[props.item?.model_type]" v-bind="props" />
<post-reaction
size="small"
:item="props.item"
:model="props.item?.model_type ? props.item?.model_type + 's' : 'articles'"
:reactions="props.item?.reaction_list ?? {}"
@update="updateReactions"
/>
</div>
</div> </div>
</template>
</v-card> <component :is="renderer[props.item?.model_type]" v-bind="props" />
<post-reaction
size="small"
:item="props.item"
:model="props.item?.model_type ? props.item?.model_type + 's' : 'articles'"
:reactions="props.item?.reaction_list ?? {}"
@update="updateReactions"
/>
</div>
</div>
</template> </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>

View File

@ -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">
<post-item brief :item="item" @update:item="val => updateItem(idx, val)" /> <v-card>
<template #text>
<post-item brief :item="item" @update:item="val => updateItem(idx, val)" />
</template>
</v-card>
</div> </div>
</template> </template>
</v-infinite-scroll> </v-infinite-scroll>

View File

@ -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 })

View File

@ -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>