💄 全新设计重构 #2
@ -1,15 +1,16 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"code.smartsheep.studio/hydrogen/identity/pkg/database"
|
"code.smartsheep.studio/hydrogen/identity/pkg/database"
|
||||||
"code.smartsheep.studio/hydrogen/identity/pkg/models"
|
"code.smartsheep.studio/hydrogen/identity/pkg/models"
|
||||||
"code.smartsheep.studio/hydrogen/identity/pkg/services"
|
"code.smartsheep.studio/hydrogen/identity/pkg/services"
|
||||||
"fmt"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func getUserinfo(c *fiber.Ctx) error {
|
func getUserinfo(c *fiber.Ctx) error {
|
||||||
@ -20,7 +21,6 @@ func getUserinfo(c *fiber.Ctx) error {
|
|||||||
Where(&models.Account{BaseModel: models.BaseModel{ID: user.ID}}).
|
Where(&models.Account{BaseModel: models.BaseModel{ID: user.ID}}).
|
||||||
Preload("Profile").
|
Preload("Profile").
|
||||||
Preload("Contacts").
|
Preload("Contacts").
|
||||||
Preload("Notifications", "read_at IS NULL").
|
|
||||||
First(&data).Error; err != nil {
|
First(&data).Error; err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"code.smartsheep.studio/hydrogen/identity/pkg/database"
|
"code.smartsheep.studio/hydrogen/identity/pkg/database"
|
||||||
"code.smartsheep.studio/hydrogen/identity/pkg/models"
|
"code.smartsheep.studio/hydrogen/identity/pkg/models"
|
||||||
"code.smartsheep.studio/hydrogen/identity/pkg/services"
|
"code.smartsheep.studio/hydrogen/identity/pkg/services"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func getNotifications(c *fiber.Ctx) error {
|
func getNotifications(c *fiber.Ctx) error {
|
||||||
@ -14,7 +15,7 @@ func getNotifications(c *fiber.Ctx) error {
|
|||||||
take := c.QueryInt("take", 0)
|
take := c.QueryInt("take", 0)
|
||||||
offset := c.QueryInt("offset", 0)
|
offset := c.QueryInt("offset", 0)
|
||||||
|
|
||||||
only_unread := c.QueryBool("only_unread", true)
|
only_unread := !c.QueryBool("past", false)
|
||||||
|
|
||||||
tx := database.C.Where(&models.Notification{RecipientID: user.ID}).Model(&models.Notification{})
|
tx := database.C.Where(&models.Notification{RecipientID: user.ID}).Model(&models.Notification{})
|
||||||
if only_unread {
|
if only_unread {
|
||||||
|
87
pkg/views/src/components/NotificationList.vue
Normal file
87
pkg/views/src/components/NotificationList.vue
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<v-menu eager :close-on-content-click="false">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<v-btn v-bind="props" stacked rounded="circle" size="small" variant="text" :loading="loading">
|
||||||
|
<v-badge v-if="pagination.total > 0" color="error" :content="pagination.total">
|
||||||
|
<v-icon icon="mdi-bell" />
|
||||||
|
</v-badge>
|
||||||
|
|
||||||
|
<v-icon v-else icon="mdi-bell" />
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list v-if="notifications.length <= 0" class="w-[380px]" density="compact">
|
||||||
|
<v-list-item>
|
||||||
|
<v-alert class="text-sm" variant="tonal" type="info">You are done! There is no unread notifications for you.</v-alert>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<v-list v-else class="w-[380px]" density="compact" lines="three">
|
||||||
|
<v-list-item v-for="item in notifications">
|
||||||
|
<template #title>{{ item.subject }}</template>
|
||||||
|
<template #subtitle>{{ item.content }}</template>
|
||||||
|
|
||||||
|
<template #append>
|
||||||
|
<v-btn icon="mdi-check" size="x-small" variant="text" :disabled="loading" @click="markAsRead(item)" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="flex text-xs gap-1">
|
||||||
|
<a v-for="link in item.links" class="mt-1 underline" target="_blank" :href="link.url">{{ link.label }}</a>
|
||||||
|
</div>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
|
||||||
|
<!-- @vue-ignore -->
|
||||||
|
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { request } from "@/scripts/request"
|
||||||
|
import { getAtk } from "@/stores/userinfo"
|
||||||
|
import { reactive, ref } from "vue"
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
|
const notifications = ref<any[]>([])
|
||||||
|
const pagination = reactive({ page: 1, pageSize: 25, total: 0 })
|
||||||
|
|
||||||
|
async function readNotifications() {
|
||||||
|
loading.value = true
|
||||||
|
const res = await request(
|
||||||
|
"/api/notifications?" +
|
||||||
|
new URLSearchParams({
|
||||||
|
take: pagination.pageSize.toString(),
|
||||||
|
offset: ((pagination.page - 1) * pagination.pageSize).toString(),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
headers: { Authorization: `Bearer ${getAtk()}` },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (res.status === 200) {
|
||||||
|
const data = await res.json()
|
||||||
|
notifications.value = data["data"]
|
||||||
|
pagination.total = data["count"]
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
readNotifications()
|
||||||
|
|
||||||
|
async function markAsRead(item: any) {
|
||||||
|
loading.value = true
|
||||||
|
const res = await request(`/api/notifications/${item.id}/read`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { Authorization: `Bearer ${getAtk()}` },
|
||||||
|
})
|
||||||
|
if (res.status !== 200) {
|
||||||
|
error.value = await res.text()
|
||||||
|
} else {
|
||||||
|
await readNotifications()
|
||||||
|
error.value = null
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
</script>
|
43
pkg/views/src/components/UserMenu.vue
Normal file
43
pkg/views/src/components/UserMenu.vue
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<v-menu>
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<v-btn flat exact v-bind="props" icon>
|
||||||
|
<v-avatar color="transparent" icon="mdi-account-circle" :image="'/api/avatar/' + id.userinfo.data?.avatar" />
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list density="compact" v-if="!id.userinfo.isLoggedIn">
|
||||||
|
<v-list-item title="Sign in" prepend-icon="mdi-login-variant" :to="{ name: 'auth.sign-in' }" />
|
||||||
|
<v-list-item title="Create account" prepend-icon="mdi-account-plus" :to="{ name: 'auth.sign-up' }" />
|
||||||
|
</v-list>
|
||||||
|
<v-list density="compact" v-else>
|
||||||
|
<v-list-item :title="nickname" :subtitle="username" />
|
||||||
|
|
||||||
|
<v-divider class="border-opacity-50 my-2" />
|
||||||
|
|
||||||
|
<v-list-item title="User Center" prepend-icon="mdi-account-supervisor" exact :to="{ name: 'dashboard' }" />
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useUserinfo } from "@/stores/userinfo"
|
||||||
|
import { computed } from "vue"
|
||||||
|
|
||||||
|
const id = useUserinfo()
|
||||||
|
|
||||||
|
const username = computed(() => {
|
||||||
|
if (id.userinfo.isLoggedIn) {
|
||||||
|
return "@" + id.userinfo.data?.name
|
||||||
|
} else {
|
||||||
|
return "@vistor"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const nickname = computed(() => {
|
||||||
|
if (id.userinfo.isLoggedIn) {
|
||||||
|
return id.userinfo.data?.nick
|
||||||
|
} else {
|
||||||
|
return "Anonymous"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
@ -7,21 +7,13 @@
|
|||||||
|
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
|
|
||||||
<v-menu>
|
<div class="me-2">
|
||||||
<template #activator="{ props }">
|
<notification-list />
|
||||||
<v-btn flat exact v-bind="props" icon>
|
</div>
|
||||||
<v-avatar color="transparent" icon="mdi-account-circle" :image="'/api/avatar/' + id.userinfo.data?.avatar" />
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<v-list density="compact" v-if="!id.userinfo.isLoggedIn">
|
<div>
|
||||||
<v-list-item title="Sign in" prepend-icon="mdi-login-variant" :to="{ name: 'auth.sign-in' }" />
|
<user-menu />
|
||||||
<v-list-item title="Create account" prepend-icon="mdi-account-plus" :to="{ name: 'auth.sign-up' }" />
|
</div>
|
||||||
</v-list>
|
|
||||||
<v-list density="compact" v-else>
|
|
||||||
<v-list-item title="User Center" prepend-icon="mdi-account-supervisor" exact :to="{ name: 'dashboard' }" />
|
|
||||||
</v-list>
|
|
||||||
</v-menu>
|
|
||||||
</div>
|
</div>
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
|
|
||||||
@ -31,26 +23,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue"
|
|
||||||
import { useUserinfo } from "@/stores/userinfo"
|
import { useUserinfo } from "@/stores/userinfo"
|
||||||
|
import NotificationList from "@/components/NotificationList.vue"
|
||||||
|
import UserMenu from "@/components/UserMenu.vue"
|
||||||
|
|
||||||
const id = useUserinfo()
|
const id = useUserinfo()
|
||||||
|
|
||||||
const username = computed(() => {
|
|
||||||
if (id.userinfo.isLoggedIn) {
|
|
||||||
return "@" + id.userinfo.data?.name
|
|
||||||
} else {
|
|
||||||
return "@vistor"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const nickname = computed(() => {
|
|
||||||
if (id.userinfo.isLoggedIn) {
|
|
||||||
return id.userinfo.data?.nick
|
|
||||||
} else {
|
|
||||||
return "Anonymous"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
id.readProfiles()
|
id.readProfiles()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -105,7 +105,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<v-tooltip :text="item.user_agent" location="top">
|
<v-tooltip :text="item.user_agent" location="top">
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
<div v-bind="props" class="text-ellipsis whitespace-nowrap overflow-hidden max-w-[280px]">
|
<div v-bind="props" class="text-ellipsis whitespace-nowrap overflow-hidden max-w-[180px]">
|
||||||
{{ item.user_agent }}
|
{{ item.user_agent }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
Loading…
Reference in New Issue
Block a user