feat: 添加通知功能,集成模拟数据并更新通知视图
This commit is contained in:
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
@@ -161,3 +161,5 @@ Capacitor 配置本地开发服务器地址为 http://localhost:5173
|
||||
国际化支持中文和英文,配置文件在 src/locales/ 目前多语言只需要支持中英文,其他语言先不要更改
|
||||
样式使用 TailwindCSS 4.x + Ionic CSS Variables 混合模式
|
||||
函数风格使用function关键字定义,一般不要使用箭头函数
|
||||
如果有任何不明确的地方,随时提问以获取更多信息
|
||||
如果代码有错误,请先执行 pnpm run lint:fix 来自动修复代码问题
|
||||
|
||||
10
components.d.ts
vendored
10
components.d.ts
vendored
@@ -18,6 +18,7 @@ declare module 'vue' {
|
||||
IonApp: typeof import('@ionic/vue')['IonApp']
|
||||
IonAvatar: typeof import('@ionic/vue')['IonAvatar']
|
||||
IonBackButton: typeof import('@ionic/vue')['IonBackButton']
|
||||
IonBadge: typeof import('@ionic/vue')['IonBadge']
|
||||
IonButton: typeof import('@ionic/vue')['IonButton']
|
||||
IonButtons: typeof import('@ionic/vue')['IonButtons']
|
||||
IonCol: typeof import('@ionic/vue')['IonCol']
|
||||
@@ -32,6 +33,9 @@ declare module 'vue' {
|
||||
IonInfiniteScrollContent: typeof import('@ionic/vue')['IonInfiniteScrollContent']
|
||||
IonInput: typeof import('@ionic/vue')['IonInput']
|
||||
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']
|
||||
IonList: typeof import('@ionic/vue')['IonList']
|
||||
IonListHeader: typeof import('@ionic/vue')['IonListHeader']
|
||||
@@ -49,6 +53,7 @@ declare module 'vue' {
|
||||
IonSegmentButton: typeof import('@ionic/vue')['IonSegmentButton']
|
||||
IonSelect: typeof import('@ionic/vue')['IonSelect']
|
||||
IonSelectOption: typeof import('@ionic/vue')['IonSelectOption']
|
||||
IonSpinner: typeof import('@ionic/vue')['IonSpinner']
|
||||
IonTabBar: typeof import('@ionic/vue')['IonTabBar']
|
||||
IonTabButton: typeof import('@ionic/vue')['IonTabButton']
|
||||
IonTabs: typeof import('@ionic/vue')['IonTabs']
|
||||
@@ -71,6 +76,7 @@ declare global {
|
||||
const IonApp: typeof import('@ionic/vue')['IonApp']
|
||||
const IonAvatar: typeof import('@ionic/vue')['IonAvatar']
|
||||
const IonBackButton: typeof import('@ionic/vue')['IonBackButton']
|
||||
const IonBadge: typeof import('@ionic/vue')['IonBadge']
|
||||
const IonButton: typeof import('@ionic/vue')['IonButton']
|
||||
const IonButtons: typeof import('@ionic/vue')['IonButtons']
|
||||
const IonCol: typeof import('@ionic/vue')['IonCol']
|
||||
@@ -85,6 +91,9 @@ declare global {
|
||||
const IonInfiniteScrollContent: typeof import('@ionic/vue')['IonInfiniteScrollContent']
|
||||
const IonInput: typeof import('@ionic/vue')['IonInput']
|
||||
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 IonList: typeof import('@ionic/vue')['IonList']
|
||||
const IonListHeader: typeof import('@ionic/vue')['IonListHeader']
|
||||
@@ -102,6 +111,7 @@ declare global {
|
||||
const IonSegmentButton: typeof import('@ionic/vue')['IonSegmentButton']
|
||||
const IonSelect: typeof import('@ionic/vue')['IonSelect']
|
||||
const IonSelectOption: typeof import('@ionic/vue')['IonSelectOption']
|
||||
const IonSpinner: typeof import('@ionic/vue')['IonSpinner']
|
||||
const IonTabBar: typeof import('@ionic/vue')['IonTabBar']
|
||||
const IonTabButton: typeof import('@ionic/vue')['IonTabButton']
|
||||
const IonTabs: typeof import('@ionic/vue')['IonTabs']
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { WatchSource } from "vue";
|
||||
import { treaty } from "@elysiajs/eden";
|
||||
import { toastController } from "@ionic/vue";
|
||||
import { i18n } from "@/locales";
|
||||
import { useMocks } from "@/mocks";
|
||||
|
||||
const client = treaty<App>(window.location.origin, {
|
||||
fetch: {
|
||||
@@ -121,4 +122,17 @@ export function safeClient<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 };
|
||||
|
||||
42
src/main.ts
42
src/main.ts
@@ -5,7 +5,7 @@ import uiComponents from "@/ui";
|
||||
import App from "./App.vue";
|
||||
import { authClient } from "./auth";
|
||||
import { i18n } from "./locales";
|
||||
|
||||
import { setupMocks } from "./mocks";
|
||||
import { router } from "./router";
|
||||
|
||||
/* Core CSS required for Ionic components to work properly */
|
||||
@@ -41,26 +41,32 @@ import "./theme/ionic.css";
|
||||
|
||||
useTheme();
|
||||
|
||||
authClient.getSession().then((session) => {
|
||||
const pinia = createPinia();
|
||||
const userStore = useUserStore(pinia);
|
||||
userStore.setToken(session.data?.session.token || "");
|
||||
function initApp() {
|
||||
authClient.getSession().then((session) => {
|
||||
const pinia = createPinia();
|
||||
const userStore = useUserStore(pinia);
|
||||
userStore.setToken(session.data?.session.token || "");
|
||||
|
||||
const app = createApp(App)
|
||||
.use(IonicVue, {
|
||||
backButtonText: "返回",
|
||||
mode: "ios",
|
||||
statusTap: true,
|
||||
swipeBackEnabled: true,
|
||||
const app = createApp(App)
|
||||
.use(IonicVue, {
|
||||
backButtonText: "返回",
|
||||
mode: "ios",
|
||||
statusTap: true,
|
||||
swipeBackEnabled: true,
|
||||
// rippleEffect: true,
|
||||
// animated: false,
|
||||
})
|
||||
.use(uiComponents)
|
||||
.use(pinia)
|
||||
.use(router)
|
||||
.use(i18n);
|
||||
})
|
||||
.use(uiComponents)
|
||||
.use(pinia)
|
||||
.use(router)
|
||||
.use(i18n);
|
||||
|
||||
router.isReady().then(() => {
|
||||
app.mount("#app");
|
||||
router.isReady().then(() => {
|
||||
app.mount("#app");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setupMocks().then(() => {
|
||||
initApp();
|
||||
});
|
||||
|
||||
1
src/mocks/data/index.ts
Normal file
1
src/mocks/data/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
import "./notify";
|
||||
80
src/mocks/data/notify.ts
Normal file
80
src/mocks/data/notify.ts
Normal 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
41
src/mocks/index.ts
Normal 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();
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import IcBaselineNotificationsNone from "~icons/ic/baseline-notifications-none";
|
||||
import IconParkOutlineClearFormat from "~icons/icon-park-outline/clear-format";
|
||||
import MaterialSymbolsAndroidContacts from "~icons/material-symbols/android-contacts";
|
||||
import { mockClient } from "@/api";
|
||||
|
||||
const { data } = mockClient("notify.list");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -18,6 +22,27 @@ import MaterialSymbolsAndroidContacts from "~icons/material-symbols/android-cont
|
||||
</IonHeader>
|
||||
<IonContent :fullscreen="true">
|
||||
<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>
|
||||
</IonPage>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user