feat: 添加通知功能,整合通知数据获取和处理,优化通知列表显示
This commit is contained in:
@@ -80,6 +80,10 @@ export type HoldingsData = Treaty.Data<typeof client.api.rwa.holdings.get>;
|
|||||||
|
|
||||||
export type HoldingItem = HoldingsData extends { data: Array<infer T> } ? T : HoldingsData extends Array<infer T> ? T : never;
|
export type HoldingItem = HoldingsData extends { data: Array<infer T> } ? T : HoldingsData extends Array<infer T> ? T : never;
|
||||||
|
|
||||||
|
export type NotificationData = Treaty.Data<typeof client.api.notifications.get>["data"][number];
|
||||||
|
|
||||||
|
export type NotificationBody = TreatyQuery<typeof client.api.notifications.get>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用版本信息
|
* 应用版本信息
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,24 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import IcBaselineNotificationsNone from "~icons/ic/baseline-notifications-none";
|
import { client, safeClient } from "@/api";
|
||||||
import { mockClient } from "@/api";
|
import { NotificationTypeIcon } from "./enum";
|
||||||
|
|
||||||
const props = defineProps<{ id: string }>();
|
const props = defineProps<{ id: string }>();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { data: allNotifications } = await mockClient("notify.list");
|
const { data } = await safeClient(client.api.notifications({ id: props.id }).get());
|
||||||
|
|
||||||
const notification = computed(() => {
|
function getConfigByType(type: string) {
|
||||||
return allNotifications.value.find(item => String(item.id) === props.id);
|
return NotificationTypeIcon[type];
|
||||||
});
|
|
||||||
|
|
||||||
watch(notification, (val) => {
|
|
||||||
if (!val && allNotifications.value.length > 0) {
|
|
||||||
router.replace("/layout/notify");
|
|
||||||
}
|
|
||||||
}, { immediate: true });
|
|
||||||
|
|
||||||
function handleBack() {
|
|
||||||
router.back();
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -31,34 +20,32 @@ function handleBack() {
|
|||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</IonHeader>
|
</IonHeader>
|
||||||
<IonContent :fullscreen="true" class="ion-padding">
|
<IonContent :fullscreen="true" class="ion-padding">
|
||||||
<div v-if="notification" class="notification-detail">
|
<div v-if="data" class="notification-detail">
|
||||||
<!-- 图标和标题 -->
|
<!-- 图标和标题 -->
|
||||||
<div class="flex items-start gap-4 mb-2">
|
<div class="flex items-start gap-4">
|
||||||
<div class="bg-[#f1f1f1] dark:bg-[#2d2d2d] p-2.5 rounded-full shrink-0">
|
<div class="bg-[#f1f1f1] dark:bg-[#2d2d2d] p-2.5 rounded-full shrink-0">
|
||||||
<IcBaselineNotificationsNone class="text-2xl text-[#71cc51]" />
|
<Icon :icon="getConfigByType(data.type).icon" class="text-2xl" :style="{ color: getConfigByType(data.type).color }" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="text-xl font-semibold wrap-break-word">
|
<div class="text-xl font-semibold wrap-break-word">
|
||||||
{{ notification.title }}
|
{{ data.title }}
|
||||||
</div>
|
</div>
|
||||||
<ion-note class="text-xs">
|
<ion-note class="text-xs">
|
||||||
{{ useDateFormat(notification.date, 'YYYY-MM-DD HH:mm:ss') }}
|
{{ useDateFormat(data.createdAt, 'YYYY-MM-DD HH:mm:ss') }}
|
||||||
</ion-note>
|
</ion-note>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分割线 -->
|
|
||||||
<ui-divider />
|
<ui-divider />
|
||||||
|
|
||||||
<!-- 内容 -->
|
<!-- 内容 -->
|
||||||
<div class="notification-content">
|
<div class="notification-content">
|
||||||
<p class="text-base leading-relaxed whitespace-pre-wrap wrap-break-word">
|
<p class="text-base leading-relaxed whitespace-pre-wrap wrap-break-word">
|
||||||
{{ notification.content }}
|
{{ data.content }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 加载状态 -->
|
|
||||||
<div v-else class="flex items-center justify-center h-full">
|
<div v-else class="flex items-center justify-center h-full">
|
||||||
<ion-spinner name="crescent" />
|
<ion-spinner name="crescent" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
18
src/views/notify/enum.ts
Normal file
18
src/views/notify/enum.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export const NotificationTypeIcon = {
|
||||||
|
system: {
|
||||||
|
icon: "solar:bell-bing-linear",
|
||||||
|
color: "#46a724",
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
icon: "solar:shield-check-linear",
|
||||||
|
color: "#f52323",
|
||||||
|
},
|
||||||
|
transaction: {
|
||||||
|
icon: "solar:transfer-horizontal-linear",
|
||||||
|
color: "#007aff",
|
||||||
|
},
|
||||||
|
activity: {
|
||||||
|
icon: "solar:calendar-mark-line-duotone",
|
||||||
|
color: "#ffa22d",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
@@ -1,43 +1,110 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { InfiniteScrollCustomEvent, RefresherCustomEvent } from "@ionic/vue";
|
||||||
|
import type { NotificationBody, NotificationData } from "@/api/types";
|
||||||
|
import { toastController } from "@ionic/vue";
|
||||||
import IcBaselineNotificationsNone from "~icons/ic/baseline-notifications-none";
|
import IcBaselineNotificationsNone from "~icons/ic/baseline-notifications-none";
|
||||||
import IconParkOutlineClearFormat from "~icons/icon-park-outline/clear-format";
|
import IconParkOutlineClearFormat from "~icons/icon-park-outline/clear-format";
|
||||||
import MaterialSymbolsAndroidContacts from "~icons/material-symbols/android-contacts";
|
import MaterialSymbolsAndroidContacts from "~icons/material-symbols/android-contacts";
|
||||||
import { mockClient } from "@/api";
|
import { client, mockClient, safeClient } from "@/api";
|
||||||
|
import { NotificationTypeIcon } from "./enum";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data } = mockClient("notify.list");
|
const [query] = useResetRef<NotificationBody>({
|
||||||
|
limit: 20,
|
||||||
|
offset: 0,
|
||||||
|
});
|
||||||
|
const data = ref<NotificationData[]>([]);
|
||||||
|
const isFinished = ref(false);
|
||||||
|
|
||||||
function handleItemClick(id: number) {
|
async function fetchData() {
|
||||||
router.push(`/notify/${id}`);
|
const { data: responseData } = await safeClient(() => client.api.notifications.get({
|
||||||
|
query: query.value,
|
||||||
|
}));
|
||||||
|
data.value.push(...(responseData.value?.data || []));
|
||||||
|
isFinished.value = (responseData.value?.data.length || 0) < query.value.limit!;
|
||||||
}
|
}
|
||||||
|
function resetRwaData() {
|
||||||
|
query.value.offset = 0;
|
||||||
|
data.value = [];
|
||||||
|
isFinished.value = false;
|
||||||
|
}
|
||||||
|
async function handleRefresh(event: RefresherCustomEvent) {
|
||||||
|
resetRwaData();
|
||||||
|
await fetchData();
|
||||||
|
setTimeout(() => {
|
||||||
|
event.target.complete();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
async function handleInfinite(event: InfiniteScrollCustomEvent) {
|
||||||
|
if (isFinished.value) {
|
||||||
|
event.target.complete();
|
||||||
|
event.target.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
query.value.offset! += query.value.limit!;
|
||||||
|
await fetchData();
|
||||||
|
setTimeout(() => {
|
||||||
|
event.target.complete();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
function handleItemClick(item: NotificationData) {
|
||||||
|
safeClient(client.api.notifications({ id: item.id }).read.post()).then(() => {
|
||||||
|
item.status = "read";
|
||||||
|
});
|
||||||
|
router.push(`/notify/${item.id}`);
|
||||||
|
}
|
||||||
|
async function handleAllRead() {
|
||||||
|
await safeClient(() => client.api.notifications["batch-read"].post({
|
||||||
|
ids: [],
|
||||||
|
}));
|
||||||
|
const toast = await toastController.create({
|
||||||
|
message: "所有通知已标记为已读",
|
||||||
|
duration: 2000,
|
||||||
|
position: "bottom",
|
||||||
|
color: "success",
|
||||||
|
});
|
||||||
|
await toast.present();
|
||||||
|
data.value.forEach((item) => {
|
||||||
|
item.status = "read";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function getConfigByType(type: string) {
|
||||||
|
return NotificationTypeIcon[type];
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
fetchData();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<IonPage>
|
<IonPage>
|
||||||
<IonHeader class="ion-no-border">
|
<IonHeader class="ion-no-border">
|
||||||
<ion-toolbar class="ion-toolbar">
|
<ion-toolbar class="ion-toolbar">
|
||||||
<ion-button slot="start" fill="clear">
|
<ion-button slot="end" fill="clear" @click="handleAllRead">
|
||||||
<IconParkOutlineClearFormat slot="icon-only" />
|
<IconParkOutlineClearFormat slot="icon-only" />
|
||||||
</ion-button>
|
</ion-button>
|
||||||
<ion-button slot="end" fill="clear">
|
<!-- <ion-button slot="end" fill="clear">
|
||||||
<MaterialSymbolsAndroidContacts slot="icon-only" />
|
<MaterialSymbolsAndroidContacts slot="icon-only" />
|
||||||
</ion-button>
|
</ion-button> -->
|
||||||
<ion-title>通知</ion-title>
|
<ion-title>通知</ion-title>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</IonHeader>
|
</IonHeader>
|
||||||
<IonContent :fullscreen="true">
|
<IonContent :fullscreen="true">
|
||||||
<ion-searchbar placeholder="Search" />
|
<!-- <ion-searchbar placeholder="Search" /> -->
|
||||||
|
<ion-refresher slot="fixed" @ion-refresh="handleRefresh($event)">
|
||||||
|
<ion-refresher-content />
|
||||||
|
</ion-refresher>
|
||||||
|
|
||||||
<ion-list lines="none">
|
<ion-list lines="none">
|
||||||
<ion-item
|
<ion-item
|
||||||
v-for="item in data"
|
v-for="item in data"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="py-3"
|
class="py-3"
|
||||||
button
|
@click="handleItemClick(item)"
|
||||||
@click="handleItemClick(item.id)"
|
|
||||||
>
|
>
|
||||||
<div slot="start" class="bg-[#f4f4f4] dark:bg-[#0a0a0a] p-2.5 rounded-full">
|
<div slot="start" class="bg-[#f4f4f4] dark:bg-[#0a0a0a] p-2.5 rounded-full">
|
||||||
<IcBaselineNotificationsNone class="text-2xl text-[#46a724]" />
|
<Icon :icon="getConfigByType(item.type).icon" class="text-2xl" :style="{ color: getConfigByType(item.type).color }" />
|
||||||
</div>
|
</div>
|
||||||
<div class="pl-3 w-full">
|
<div class="pl-3 w-full">
|
||||||
<div class="flex items-center justify-between mb-1">
|
<div class="flex items-center justify-between mb-1">
|
||||||
@@ -45,10 +112,10 @@ function handleItemClick(id: number) {
|
|||||||
<div class="text-md font-semibold">
|
<div class="text-md font-semibold">
|
||||||
{{ item.title }}
|
{{ item.title }}
|
||||||
</div>
|
</div>
|
||||||
<div v-show="!item.read" class="h-2 w-2 bg-red-500 rounded-full" />
|
<div v-show="item.status === 'unread'" class="h-2 w-2 bg-red-500 rounded-full" />
|
||||||
</div>
|
</div>
|
||||||
<ion-note slot="end" class="text-xs">
|
<ion-note slot="end" class="text-xs">
|
||||||
{{ useDateFormat(item.date, 'MM/DD HH:mm') }}
|
{{ useDateFormat(item.createdAt, 'MM/DD HH:mm') }}
|
||||||
</ion-note>
|
</ion-note>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs overflow-hidden text-ellipsis">
|
<div class="text-xs overflow-hidden text-ellipsis">
|
||||||
@@ -57,6 +124,12 @@ function handleItemClick(id: number) {
|
|||||||
</div>
|
</div>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
|
<ion-infinite-scroll threshold="100px" @ion-infinite="handleInfinite">
|
||||||
|
<ion-infinite-scroll-content
|
||||||
|
loading-spinner="bubbles"
|
||||||
|
loading-text="加载中..."
|
||||||
|
/>
|
||||||
|
</ion-infinite-scroll>
|
||||||
</IonContent>
|
</IonContent>
|
||||||
</IonPage>
|
</IonPage>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user