feat: 添加通知功能,集成模拟数据并更新通知视图

This commit is contained in:
2025-12-27 01:30:37 +07:00
parent 6b7a2c7ef1
commit ee07f06d9e
8 changed files with 197 additions and 18 deletions

View File

@@ -161,3 +161,5 @@ Capacitor 配置本地开发服务器地址为 http://localhost:5173
国际化支持中文和英文,配置文件在 src/locales/ 目前多语言只需要支持中英文,其他语言先不要更改 国际化支持中文和英文,配置文件在 src/locales/ 目前多语言只需要支持中英文,其他语言先不要更改
样式使用 TailwindCSS 4.x + Ionic CSS Variables 混合模式 样式使用 TailwindCSS 4.x + Ionic CSS Variables 混合模式
函数风格使用function关键字定义一般不要使用箭头函数 函数风格使用function关键字定义一般不要使用箭头函数
如果有任何不明确的地方,随时提问以获取更多信息
如果代码有错误,请先执行 pnpm run lint:fix 来自动修复代码问题

10
components.d.ts vendored
View File

@@ -18,6 +18,7 @@ declare module 'vue' {
IonApp: typeof import('@ionic/vue')['IonApp'] IonApp: typeof import('@ionic/vue')['IonApp']
IonAvatar: typeof import('@ionic/vue')['IonAvatar'] IonAvatar: typeof import('@ionic/vue')['IonAvatar']
IonBackButton: typeof import('@ionic/vue')['IonBackButton'] IonBackButton: typeof import('@ionic/vue')['IonBackButton']
IonBadge: typeof import('@ionic/vue')['IonBadge']
IonButton: typeof import('@ionic/vue')['IonButton'] IonButton: typeof import('@ionic/vue')['IonButton']
IonButtons: typeof import('@ionic/vue')['IonButtons'] IonButtons: typeof import('@ionic/vue')['IonButtons']
IonCol: typeof import('@ionic/vue')['IonCol'] IonCol: typeof import('@ionic/vue')['IonCol']
@@ -32,6 +33,9 @@ declare module 'vue' {
IonInfiniteScrollContent: typeof import('@ionic/vue')['IonInfiniteScrollContent'] IonInfiniteScrollContent: typeof import('@ionic/vue')['IonInfiniteScrollContent']
IonInput: typeof import('@ionic/vue')['IonInput'] IonInput: typeof import('@ionic/vue')['IonInput']
IonItem: typeof import('@ionic/vue')['IonItem'] IonItem: typeof import('@ionic/vue')['IonItem']
IonItemOption: typeof import('@ionic/vue')['IonItemOption']
IonItemOptions: typeof import('@ionic/vue')['IonItemOptions']
IonItemSliding: typeof import('@ionic/vue')['IonItemSliding']
IonLabel: typeof import('@ionic/vue')['IonLabel'] IonLabel: typeof import('@ionic/vue')['IonLabel']
IonList: typeof import('@ionic/vue')['IonList'] IonList: typeof import('@ionic/vue')['IonList']
IonListHeader: typeof import('@ionic/vue')['IonListHeader'] IonListHeader: typeof import('@ionic/vue')['IonListHeader']
@@ -49,6 +53,7 @@ declare module 'vue' {
IonSegmentButton: typeof import('@ionic/vue')['IonSegmentButton'] IonSegmentButton: typeof import('@ionic/vue')['IonSegmentButton']
IonSelect: typeof import('@ionic/vue')['IonSelect'] IonSelect: typeof import('@ionic/vue')['IonSelect']
IonSelectOption: typeof import('@ionic/vue')['IonSelectOption'] IonSelectOption: typeof import('@ionic/vue')['IonSelectOption']
IonSpinner: typeof import('@ionic/vue')['IonSpinner']
IonTabBar: typeof import('@ionic/vue')['IonTabBar'] IonTabBar: typeof import('@ionic/vue')['IonTabBar']
IonTabButton: typeof import('@ionic/vue')['IonTabButton'] IonTabButton: typeof import('@ionic/vue')['IonTabButton']
IonTabs: typeof import('@ionic/vue')['IonTabs'] IonTabs: typeof import('@ionic/vue')['IonTabs']
@@ -71,6 +76,7 @@ declare global {
const IonApp: typeof import('@ionic/vue')['IonApp'] const IonApp: typeof import('@ionic/vue')['IonApp']
const IonAvatar: typeof import('@ionic/vue')['IonAvatar'] const IonAvatar: typeof import('@ionic/vue')['IonAvatar']
const IonBackButton: typeof import('@ionic/vue')['IonBackButton'] const IonBackButton: typeof import('@ionic/vue')['IonBackButton']
const IonBadge: typeof import('@ionic/vue')['IonBadge']
const IonButton: typeof import('@ionic/vue')['IonButton'] const IonButton: typeof import('@ionic/vue')['IonButton']
const IonButtons: typeof import('@ionic/vue')['IonButtons'] const IonButtons: typeof import('@ionic/vue')['IonButtons']
const IonCol: typeof import('@ionic/vue')['IonCol'] const IonCol: typeof import('@ionic/vue')['IonCol']
@@ -85,6 +91,9 @@ declare global {
const IonInfiniteScrollContent: typeof import('@ionic/vue')['IonInfiniteScrollContent'] const IonInfiniteScrollContent: typeof import('@ionic/vue')['IonInfiniteScrollContent']
const IonInput: typeof import('@ionic/vue')['IonInput'] const IonInput: typeof import('@ionic/vue')['IonInput']
const IonItem: typeof import('@ionic/vue')['IonItem'] const IonItem: typeof import('@ionic/vue')['IonItem']
const IonItemOption: typeof import('@ionic/vue')['IonItemOption']
const IonItemOptions: typeof import('@ionic/vue')['IonItemOptions']
const IonItemSliding: typeof import('@ionic/vue')['IonItemSliding']
const IonLabel: typeof import('@ionic/vue')['IonLabel'] const IonLabel: typeof import('@ionic/vue')['IonLabel']
const IonList: typeof import('@ionic/vue')['IonList'] const IonList: typeof import('@ionic/vue')['IonList']
const IonListHeader: typeof import('@ionic/vue')['IonListHeader'] const IonListHeader: typeof import('@ionic/vue')['IonListHeader']
@@ -102,6 +111,7 @@ declare global {
const IonSegmentButton: typeof import('@ionic/vue')['IonSegmentButton'] const IonSegmentButton: typeof import('@ionic/vue')['IonSegmentButton']
const IonSelect: typeof import('@ionic/vue')['IonSelect'] const IonSelect: typeof import('@ionic/vue')['IonSelect']
const IonSelectOption: typeof import('@ionic/vue')['IonSelectOption'] const IonSelectOption: typeof import('@ionic/vue')['IonSelectOption']
const IonSpinner: typeof import('@ionic/vue')['IonSpinner']
const IonTabBar: typeof import('@ionic/vue')['IonTabBar'] const IonTabBar: typeof import('@ionic/vue')['IonTabBar']
const IonTabButton: typeof import('@ionic/vue')['IonTabButton'] const IonTabButton: typeof import('@ionic/vue')['IonTabButton']
const IonTabs: typeof import('@ionic/vue')['IonTabs'] const IonTabs: typeof import('@ionic/vue')['IonTabs']

View File

@@ -3,6 +3,7 @@ import type { WatchSource } from "vue";
import { treaty } from "@elysiajs/eden"; import { treaty } from "@elysiajs/eden";
import { toastController } from "@ionic/vue"; import { toastController } from "@ionic/vue";
import { i18n } from "@/locales"; import { i18n } from "@/locales";
import { useMocks } from "@/mocks";
const client = treaty<App>(window.location.origin, { const client = treaty<App>(window.location.origin, {
fetch: { fetch: {
@@ -121,4 +122,17 @@ export function safeClient<T, E>(
return promise as SafeClientReturn<T, E> & Promise<SafeClientReturn<T, E>>; return promise as SafeClientReturn<T, E> & Promise<SafeClientReturn<T, E>>;
} }
export function mockClient<T = any, E = any>(key: string): SafeClientReturn<T, E> & Promise<SafeClientReturn<T, E>> {
const requestPromise = async () => {
const mocks = useMocks();
const mockFn = mocks.get(key);
if (!mockFn) {
throw new Error(`Mock with key "${key}" not found.`);
}
const data = await mockFn();
return { data: data as T, error: null as E };
};
return safeClient<T, E>(requestPromise, { immediate: true });
}
export { client }; export { client };

View File

@@ -5,7 +5,7 @@ import uiComponents from "@/ui";
import App from "./App.vue"; import App from "./App.vue";
import { authClient } from "./auth"; import { authClient } from "./auth";
import { i18n } from "./locales"; import { i18n } from "./locales";
import { setupMocks } from "./mocks";
import { router } from "./router"; import { router } from "./router";
/* Core CSS required for Ionic components to work properly */ /* Core CSS required for Ionic components to work properly */
@@ -41,6 +41,7 @@ import "./theme/ionic.css";
useTheme(); useTheme();
function initApp() {
authClient.getSession().then((session) => { authClient.getSession().then((session) => {
const pinia = createPinia(); const pinia = createPinia();
const userStore = useUserStore(pinia); const userStore = useUserStore(pinia);
@@ -64,3 +65,8 @@ authClient.getSession().then((session) => {
app.mount("#app"); app.mount("#app");
}); });
}); });
}
setupMocks().then(() => {
initApp();
});

1
src/mocks/data/index.ts Normal file
View File

@@ -0,0 +1 @@
import "./notify";

80
src/mocks/data/notify.ts Normal file
View File

@@ -0,0 +1,80 @@
import { useMocks } from "..";
const mocks = useMocks();
mocks.register("notify.list", () => {
const now = new Date();
return [
{
id: 1,
title: "系统通知",
content: "您的账户安全等级已提升至 LV2现在可以享受更高的交易限额。",
date: new Date(now.getTime() - 1000 * 60 * 30).toISOString(), // 30分钟前
read: false,
},
{
id: 2,
title: "交易通知",
content: "您的买入订单已成功成交,交易对: BTC/USDT数量: 0.005 BTC",
date: new Date(now.getTime() - 1000 * 60 * 60 * 2).toISOString(), // 2小时前
read: false,
},
{
id: 3,
title: "活动通知",
content: "新春特惠活动火热进行中!邀请好友注册交易即可获得最高 100 USDT 奖励活动截止时间2025-02-28",
date: new Date(now.getTime() - 1000 * 60 * 60 * 5).toISOString(), // 5小时前
read: true,
},
{
id: 4,
title: "安全提醒",
content: "检测到您的账户在新设备登录,登录地点:北京,如非本人操作,请立即修改密码并联系客服。",
date: new Date(now.getTime() - 1000 * 60 * 60 * 24).toISOString(), // 1天前
read: false,
},
{
id: 5,
title: "交易通知",
content: "您的卖出订单已成功成交,交易对: ETH/USDT数量: 0.1 ETH成交价格: 2850 USDT",
date: new Date(now.getTime() - 1000 * 60 * 60 * 24 * 2).toISOString(), // 2天前
read: true,
},
{
id: 6,
title: "系统通知",
content: "平台将于北京时间 2025-01-05 02:00 至 04:00 进行系统升级维护,期间部分功能可能暂时无法使用。",
date: new Date(now.getTime() - 1000 * 60 * 60 * 24 * 3).toISOString(), // 3天前
read: true,
},
{
id: 7,
title: "活动通知",
content: "恭喜您获得新手礼包!包含 10 USDT 体验金和 7 天 VIP 会员权益,请及时领取。",
date: new Date(now.getTime() - 1000 * 60 * 60 * 24 * 5).toISOString(), // 5天前
read: true,
},
{
id: 8,
title: "安全提醒",
content: "您的提现申请已通过审核,预计将在 24 小时内到账,请注意查收。",
date: new Date(now.getTime() - 1000 * 60 * 60 * 24 * 7).toISOString(), // 7天前
read: true,
},
{
id: 9,
title: "交易通知",
content: "市场波动较大,您持仓的 BTC 当前盈利已达 15%,是否考虑止盈?",
date: new Date(now.getTime() - 1000 * 60 * 60 * 24 * 10).toISOString(), // 10天前
read: true,
},
{
id: 10,
title: "系统通知",
content: "为了更好地保护您的资产安全建议您开启双重验证2FA功能。",
date: new Date(now.getTime() - 1000 * 60 * 60 * 24 * 15).toISOString(), // 15天前
read: true,
},
];
});

41
src/mocks/index.ts Normal file
View File

@@ -0,0 +1,41 @@
import type { Awaitable } from "@vueuse/core";
export class Mock {
private mocks: Map<string, () => Awaitable<any>>;
constructor() {
this.mocks = new Map();
}
get keys(): string[] {
return Array.from(this.mocks.keys());
}
register(key: string, data: () => Awaitable<any>) {
this.mocks.set(key, data);
}
unregister(key: string) {
this.mocks.delete(key);
}
get(key: string): (() => Awaitable<any>) | undefined {
return this.mocks.get(key);
}
}
export function useMocks() {
const singletonKey = "__mocks_singleton__";
if (!(globalThis as any)[singletonKey]) {
(globalThis as any)[singletonKey] = new Mock();
}
return (globalThis as any)[singletonKey] as Mock;
}
export function setupMocks() {
return new Promise<void>((resolve) => {
if (import.meta.env.DEV) {
import("./data");
}
resolve();
});
}

View File

@@ -1,6 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
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";
const { data } = mockClient("notify.list");
</script> </script>
<template> <template>
@@ -18,6 +22,27 @@ import MaterialSymbolsAndroidContacts from "~icons/material-symbols/android-cont
</IonHeader> </IonHeader>
<IonContent :fullscreen="true"> <IonContent :fullscreen="true">
<ion-searchbar placeholder="Search" /> <ion-searchbar placeholder="Search" />
<ion-list lines="none">
<ion-item v-for="item in data" :key="item.id" class="py-3">
<div slot="start" class="bg-text-900 p-2.5 rounded-full">
<IcBaselineNotificationsNone class="text-2xl text-(--ion-color-success-tint)" />
</div>
<div class="pl-3 w-full">
<div class="flex items-center justify-between mb-1">
<div class="text-md font-semibold">
{{ item.title }}
</div>
<ion-note slot="end" class="text-xs">
{{ useDateFormat(item.date, 'MM/DD HH:mm') }}
</ion-note>
</div>
<div class="text-xs overflow-hidden text-ellipsis">
{{ item.content }}
</div>
</div>
</ion-item>
</ion-list>
</IonContent> </IonContent>
</IonPage> </IonPage>
</template> </template>